<p class="hint">Universe: integers <b>0–63</b>. Click a cell to <b>insert</b> (green) or remove it. Then type a key and press <b>Predecessor</b> or <b>Successor</b> to watch the vEB cluster descent.</p>
<div id="grid"></div>
<div class="controls">
<input id="qval" type="number" min="0" max="63" value="30" placeholder="0–63">
<button id="pred">Predecessor</button>
<button id="succ">Successor</button>
<button id="clear" class="ghost">Clear</button>
</div>
<div id="trace"></div>
<div id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#grid { display: grid; grid-template-columns: repeat(16, 1fr); gap: 3px; margin-bottom: .7rem; }
.cell { aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
font: 600 9px ui-monospace, monospace; border-radius: 4px; cursor: pointer;
background: #d4d8dd; color: #555; user-select: none; transition: background .12s; }
.cell:hover { background: #bcc0c6; }
.cell.in { background: #2a9d5c; color: #fff; }
.cell.hit { background: #e63946; color: #fff; }
.cell.step { background: #f4a833; color: #fff; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; }
input[type=number] { width: 72px; padding: .4rem .5rem; border: 1px solid #adb1b8; border-radius: 8px;
font: 14px system-ui; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#trace { font: 13px ui-monospace, monospace; background: #f4f6f8; border-radius: 8px;
padding: .55rem .75rem; min-height: 2.4em; color: #333; white-space: pre-wrap;
line-height: 1.55; margin-bottom: .4rem; }
#result { font: 700 1rem system-ui; min-height: 1.4em; }
.ok { color: #0a7d33; }
.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Van Emde Boas tree simulation over universe U=64 (keys 0..63).
// We store inserted keys in a plain sorted array (small enough) but
// SIMULATE the vEB cluster descent faithfully, logging each step.
// The key property: at each level with universe size u, we split key k
// into high = Math.floor(k / Math.sqrt(u)) and low = k % Math.sqrt(u).
// We descend into the cluster indexed by 'high', of size sqrt(u). Levels: u=64 -> u=8 -> u=2 (base). Steps per query <= 2 * log2(log2(64)) = 6.
const U = 64;
const stored = new Set();
const cells = [];
function sqrtU(u) { return Math.round(Math.sqrt(u)); }
function high(k, u) { return Math.floor(k / sqrtU(u)); }
function low(k, u) { return k % sqrtU(u); }
function index(h, l, u) { return h * sqrtU(u) + l; }
// Build grid
const grid = document.getElementById('grid');
for (let i = 0; i < U; i++) {
const d = document.createElement('div');
d.className = 'cell';
d.textContent = i;
d.addEventListener('click', () => {
stored.has(i) ? stored.delete(i) : stored.add(i);
renderGrid([]);
document.getElementById('trace').textContent = '';
document.getElementById('result').textContent = '';
});
cells.push(d);
grid.appendChild(d);
}
function renderGrid(highlights) {
cells.forEach((c, i) => {
c.className = 'cell' + (stored.has(i) ? ' in' : '');
});
highlights.forEach(({ key, cls }) => {
if (key >= 0 && key < U) cells[key].className = 'cell ' + cls;
});
}
// Simulate vEB predecessor query with step logging.
// Returns { result: number|null, steps: string[] }
function vebPredecessor(query) {
const keys = [...stored].sort((a, b) => a - b);
if (keys.length === 0) return { result: null, steps: ['Tree is empty.'] };
const steps = [];
const visitedKeys = [];
function descend(q, u, offset) {
if (u <= 2) {
// Base case: universe {0,1}; check both
steps.push(" " + fmt('[u=%u%, offset=%offset%] Base case. Check %offset% and %v%.', { u: u, offset: offset, v: offset+1 }));
// largest stored key <= offset+q
const target = offset + q;
const found = keys.filter(k => k <= target && k >= offset).sort((a,b)=>b-a)[0];
if (found !== undefined) {
steps.push(" " + fmt('→ found %found% in base cluster.', { found: found }));
visitedKeys.push({ key: found, cls: 'hit' });
}
return found !== undefined ? found : null;
}
const h = high(q, u);
const l = low(q, u);
const clusterSize = sqrtU(u);
const clusterOffset = offset + h * clusterSize;
steps.push(`u=${u}: key=${offset+q}, cluster[${h}] covers [${clusterOffset}..${clusterOffset+clusterSize-1}], low=${l}`);
// Check if cluster h has any key <= offset+q
const inCluster = keys.filter(k => k >= clusterOffset && k <= clusterOffset + l);
if (inCluster.length > 0) {
steps.push(" " + fmt('Cluster %h% non-empty below low=%l%. Descend into cluster.', { h: h, l: l }));
return descend(l, clusterSize, clusterOffset);
} else {
steps.push(" " + fmt('Cluster %h% empty below low=%l%. Consult summary for prev non-empty cluster.', { h: h, l: l }));
// Find previous non-empty cluster < h
let prevCluster = -1;
for (let c = h - 1; c >= 0; c--) {
const cOff = offset + c * clusterSize;
if (keys.some(k => k >= cOff && k < cOff + clusterSize)) {
prevCluster = c;
break;
}
}
if (prevCluster === -1) {
steps.push(" " + 'No earlier cluster has elements. No predecessor in this sub-tree.');
return null;
}
steps.push(" " + fmt('Prev non-empty cluster: %prevCluster%. Descend for its max.', { prevCluster: prevCluster }));
const pcOff = offset + prevCluster * clusterSize;
const maxInPrev = keys.filter(k => k >= pcOff && k < pcOff + clusterSize).sort((a,b)=>b-a)[0];
visitedKeys.push({ key: maxInPrev, cls: 'hit' });
steps.push(" " + fmt('Max in cluster %prevCluster%: %maxInPrev%', { prevCluster: prevCluster, maxInPrev: maxInPrev }));
return maxInPrev;
}
}
const q = Math.min(Math.max(query, 0), U - 1);
steps.push(fmt('Query: predecessor(%q%), U=%U%', { q: q, U: U }));
const res = descend(q, U, 0);
return { result: res, steps, visitedKeys };
}
function vebSuccessor(query) {
const keys = [...stored].sort((a, b) => a - b);
if (keys.length === 0) return { result: null, steps: ['Tree is empty.'], visitedKeys: [] };
const steps = [];
const visitedKeys = [];
function descend(q, u, offset) {
if (u <= 2) {
steps.push(" " + fmt('[u=%u%, offset=%offset%] Base case. Check %offset% and %v%.', { u: u, offset: offset, v: offset+1 }));
const target = offset + q;
const found = keys.filter(k => k >= target && k < offset + u).sort((a,b)=>a-b)[0];
if (found !== undefined) {
steps.push(" " + fmt('→ found %found% in base cluster.', { found: found }));
visitedKeys.push({ key: found, cls: 'hit' });
}
return found !== undefined ? found : null;
}
const h = high(q, u);
const l = low(q, u);
const clusterSize = sqrtU(u);
const clusterOffset = offset + h * clusterSize;
steps.push(`u=${u}: key=${offset+q}, cluster[${h}] covers [${clusterOffset}..${clusterOffset+clusterSize-1}], low=${l}`);
// Check if cluster h has any key >= offset+q
const inCluster = keys.filter(k => k >= clusterOffset + l && k < clusterOffset + clusterSize);
if (inCluster.length > 0) {
steps.push(" " + fmt('Cluster %h% non-empty at/above low=%l%. Descend into cluster.', { h: h, l: l }));
return descend(l, clusterSize, clusterOffset);
} else {
steps.push(" " + fmt('Cluster %h% empty at/above low=%l%. Consult summary for next non-empty cluster.', { h: h, l: l }));
let nextCluster = -1;
for (let c = h + 1; c < sqrtU(u); c++) {
const cOff = offset + c * clusterSize;
if (keys.some(k => k >= cOff && k < cOff + clusterSize)) {
nextCluster = c;
break;
}
}
if (nextCluster === -1) {
steps.push(" " + 'No later cluster has elements. No successor in this sub-tree.');
return null;
}
steps.push(" " + fmt('Next non-empty cluster: %nextCluster%. Descend for its min.', { nextCluster: nextCluster }));
const ncOff = offset + nextCluster * clusterSize;
const minInNext = keys.filter(k => k >= ncOff && k < ncOff + clusterSize).sort((a,b)=>a-b)[0];
visitedKeys.push({ key: minInNext, cls: 'hit' });
steps.push(" " + fmt('Min in cluster %nextCluster%: %minInNext%', { nextCluster: nextCluster, minInNext: minInNext }));
return minInNext;
}
}
const q = Math.min(Math.max(query, 0), U - 1);
steps.push(fmt('Query: successor(%q%), U=%U%', { q: q, U: U }));
const res = descend(q, U, 0);
return { result: res, steps, visitedKeys };
}
function runQuery(mode) {
const q = parseInt(document.getElementById('qval').value, 10);
if (isNaN(q) || q < 0 || q > 63) {
document.getElementById('result').textContent = 'Enter a key 0–63.';
document.getElementById('result').className = 'bad';
return;
}
const { result, steps, visitedKeys } = mode === 'pred' ? vebPredecessor(q) : vebSuccessor(q);
document.getElementById('trace').textContent = steps.join('\n');
const resEl = document.getElementById('result');
const label = mode === 'pred' ? 'Predecessor' : 'Successor';
const labelLower = mode === 'pred' ? 'predecessor' : 'successor';
if (result === null) {
resEl.textContent = fmt('No %labelLower% found.', { labelLower: labelLower });
resEl.className = 'bad';
renderGrid([]);
} else {
resEl.textContent = fmt('%label% of %q% = %result%', { label: label, q: q, result: result });
resEl.className = 'ok';
renderGrid([{ key: q, cls: 'step' }, ...visitedKeys]);
}
// Log for verification
console.log(label + '(' + q + ') =', result, '| steps:', steps.length);
}
document.getElementById('pred').addEventListener('click', () => runQuery('pred'));
document.getElementById('succ').addEventListener('click', () => runQuery('succ'));
document.getElementById('clear').addEventListener('click', () => {
stored.clear();
renderGrid([]);
document.getElementById('trace').textContent = '';
document.getElementById('result').textContent = '';
});
// Seed with a few initial values for a nice first look
[5, 12, 23, 31, 40, 47, 58].forEach(k => stored.add(k));
renderGrid([]);
<p class="hint">Universe: integers <b>0–63</b>. Click a cell to <b>insert</b> (green) or remove it. Then type a key and press <b>Predecessor</b> or <b>Successor</b> to watch the vEB cluster descent.</p>
<div id="grid"></div>
<div class="controls">
<input id="qval" type="number" min="0" max="63" value="30" placeholder="0–63">
<button id="pred">Predecessor</button>
<button id="succ">Successor</button>
<button id="clear" class="ghost">Clear</button>
</div>
<div id="trace"></div>
<div id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#grid { display: grid; grid-template-columns: repeat(16, 1fr); gap: 3px; margin-bottom: .7rem; }
.cell { aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
font: 600 9px ui-monospace, monospace; border-radius: 4px; cursor: pointer;
background: #d4d8dd; color: #555; user-select: none; transition: background .12s; }
.cell:hover { background: #bcc0c6; }
.cell.in { background: #2a9d5c; color: #fff; }
.cell.hit { background: #e63946; color: #fff; }
.cell.step { background: #f4a833; color: #fff; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; }
input[type=number] { width: 72px; padding: .4rem .5rem; border: 1px solid #adb1b8; border-radius: 8px;
font: 14px system-ui; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#trace { font: 13px ui-monospace, monospace; background: #f4f6f8; border-radius: 8px;
padding: .55rem .75rem; min-height: 2.4em; color: #333; white-space: pre-wrap;
line-height: 1.55; margin-bottom: .4rem; }
#result { font: 700 1rem system-ui; min-height: 1.4em; }
.ok { color: #0a7d33; }
.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Van Emde Boas tree simulation over universe U=64 (keys 0..63).
// We store inserted keys in a plain sorted array (small enough) but
// SIMULATE the vEB cluster descent faithfully, logging each step.
// The key property: at each level with universe size u, we split key k
// into high = Math.floor(k / Math.sqrt(u)) and low = k % Math.sqrt(u).
// We descend into the cluster indexed by 'high', of size sqrt(u). Levels: u=64 -> u=8 -> u=2 (base). Steps per query <= 2 * log2(log2(64)) = 6.
const U = 64;
const stored = new Set();
const cells = [];
function sqrtU(u) { return Math.round(Math.sqrt(u)); }
function high(k, u) { return Math.floor(k / sqrtU(u)); }
function low(k, u) { return k % sqrtU(u); }
function index(h, l, u) { return h * sqrtU(u) + l; }
// Build grid
const grid = document.getElementById('grid');
for (let i = 0; i < U; i++) {
const d = document.createElement('div');
d.className = 'cell';
d.textContent = i;
d.addEventListener('click', () => {
stored.has(i) ? stored.delete(i) : stored.add(i);
renderGrid([]);
document.getElementById('trace').textContent = '';
document.getElementById('result').textContent = '';
});
cells.push(d);
grid.appendChild(d);
}
function renderGrid(highlights) {
cells.forEach((c, i) => {
c.className = 'cell' + (stored.has(i) ? ' in' : '');
});
highlights.forEach(({ key, cls }) => {
if (key >= 0 && key < U) cells[key].className = 'cell ' + cls;
});
}
// Simulate vEB predecessor query with step logging.
// Returns { result: number|null, steps: string[] }
function vebPredecessor(query) {
const keys = [...stored].sort((a, b) => a - b);
if (keys.length === 0) return { result: null, steps: ['Tree is empty.'] };
const steps = [];
const visitedKeys = [];
function descend(q, u, offset) {
if (u <= 2) {
// Base case: universe {0,1}; check both
steps.push(" " + fmt('[u=%u%, offset=%offset%] Base case. Check %offset% and %v%.', { u: u, offset: offset, v: offset+1 }));
// largest stored key <= offset+q
const target = offset + q;
const found = keys.filter(k => k <= target && k >= offset).sort((a,b)=>b-a)[0];
if (found !== undefined) {
steps.push(" " + fmt('→ found %found% in base cluster.', { found: found }));
visitedKeys.push({ key: found, cls: 'hit' });
}
return found !== undefined ? found : null;
}
const h = high(q, u);
const l = low(q, u);
const clusterSize = sqrtU(u);
const clusterOffset = offset + h * clusterSize;
steps.push(`u=${u}: key=${offset+q}, cluster[${h}] covers [${clusterOffset}..${clusterOffset+clusterSize-1}], low=${l}`);
// Check if cluster h has any key <= offset+q
const inCluster = keys.filter(k => k >= clusterOffset && k <= clusterOffset + l);
if (inCluster.length > 0) {
steps.push(" " + fmt('Cluster %h% non-empty below low=%l%. Descend into cluster.', { h: h, l: l }));
return descend(l, clusterSize, clusterOffset);
} else {
steps.push(" " + fmt('Cluster %h% empty below low=%l%. Consult summary for prev non-empty cluster.', { h: h, l: l }));
// Find previous non-empty cluster < h
let prevCluster = -1;
for (let c = h - 1; c >= 0; c--) {
const cOff = offset + c * clusterSize;
if (keys.some(k => k >= cOff && k < cOff + clusterSize)) {
prevCluster = c;
break;
}
}
if (prevCluster === -1) {
steps.push(" " + 'No earlier cluster has elements. No predecessor in this sub-tree.');
return null;
}
steps.push(" " + fmt('Prev non-empty cluster: %prevCluster%. Descend for its max.', { prevCluster: prevCluster }));
const pcOff = offset + prevCluster * clusterSize;
const maxInPrev = keys.filter(k => k >= pcOff && k < pcOff + clusterSize).sort((a,b)=>b-a)[0];
visitedKeys.push({ key: maxInPrev, cls: 'hit' });
steps.push(" " + fmt('Max in cluster %prevCluster%: %maxInPrev%', { prevCluster: prevCluster, maxInPrev: maxInPrev }));
return maxInPrev;
}
}
const q = Math.min(Math.max(query, 0), U - 1);
steps.push(fmt('Query: predecessor(%q%), U=%U%', { q: q, U: U }));
const res = descend(q, U, 0);
return { result: res, steps, visitedKeys };
}
function vebSuccessor(query) {
const keys = [...stored].sort((a, b) => a - b);
if (keys.length === 0) return { result: null, steps: ['Tree is empty.'], visitedKeys: [] };
const steps = [];
const visitedKeys = [];
function descend(q, u, offset) {
if (u <= 2) {
steps.push(" " + fmt('[u=%u%, offset=%offset%] Base case. Check %offset% and %v%.', { u: u, offset: offset, v: offset+1 }));
const target = offset + q;
const found = keys.filter(k => k >= target && k < offset + u).sort((a,b)=>a-b)[0];
if (found !== undefined) {
steps.push(" " + fmt('→ found %found% in base cluster.', { found: found }));
visitedKeys.push({ key: found, cls: 'hit' });
}
return found !== undefined ? found : null;
}
const h = high(q, u);
const l = low(q, u);
const clusterSize = sqrtU(u);
const clusterOffset = offset + h * clusterSize;
steps.push(`u=${u}: key=${offset+q}, cluster[${h}] covers [${clusterOffset}..${clusterOffset+clusterSize-1}], low=${l}`);
// Check if cluster h has any key >= offset+q
const inCluster = keys.filter(k => k >= clusterOffset + l && k < clusterOffset + clusterSize);
if (inCluster.length > 0) {
steps.push(" " + fmt('Cluster %h% non-empty at/above low=%l%. Descend into cluster.', { h: h, l: l }));
return descend(l, clusterSize, clusterOffset);
} else {
steps.push(" " + fmt('Cluster %h% empty at/above low=%l%. Consult summary for next non-empty cluster.', { h: h, l: l }));
let nextCluster = -1;
for (let c = h + 1; c < sqrtU(u); c++) {
const cOff = offset + c * clusterSize;
if (keys.some(k => k >= cOff && k < cOff + clusterSize)) {
nextCluster = c;
break;
}
}
if (nextCluster === -1) {
steps.push(" " + 'No later cluster has elements. No successor in this sub-tree.');
return null;
}
steps.push(" " + fmt('Next non-empty cluster: %nextCluster%. Descend for its min.', { nextCluster: nextCluster }));
const ncOff = offset + nextCluster * clusterSize;
const minInNext = keys.filter(k => k >= ncOff && k < ncOff + clusterSize).sort((a,b)=>a-b)[0];
visitedKeys.push({ key: minInNext, cls: 'hit' });
steps.push(" " + fmt('Min in cluster %nextCluster%: %minInNext%', { nextCluster: nextCluster, minInNext: minInNext }));
return minInNext;
}
}
const q = Math.min(Math.max(query, 0), U - 1);
steps.push(fmt('Query: successor(%q%), U=%U%', { q: q, U: U }));
const res = descend(q, U, 0);
return { result: res, steps, visitedKeys };
}
function runQuery(mode) {
const q = parseInt(document.getElementById('qval').value, 10);
if (isNaN(q) || q < 0 || q > 63) {
document.getElementById('result').textContent = 'Enter a key 0–63.';
document.getElementById('result').className = 'bad';
return;
}
const { result, steps, visitedKeys } = mode === 'pred' ? vebPredecessor(q) : vebSuccessor(q);
document.getElementById('trace').textContent = steps.join('\n');
const resEl = document.getElementById('result');
const label = mode === 'pred' ? 'Predecessor' : 'Successor';
const labelLower = mode === 'pred' ? 'predecessor' : 'successor';
if (result === null) {
resEl.textContent = fmt('No %labelLower% found.', { labelLower: labelLower });
resEl.className = 'bad';
renderGrid([]);
} else {
resEl.textContent = fmt('%label% of %q% = %result%', { label: label, q: q, result: result });
resEl.className = 'ok';
renderGrid([{ key: q, cls: 'step' }, ...visitedKeys]);
}
// Log for verification
console.log(label + '(' + q + ') =', result, '| steps:', steps.length);
}
document.getElementById('pred').addEventListener('click', () => runQuery('pred'));
document.getElementById('succ').addEventListener('click', () => runQuery('succ'));
document.getElementById('clear').addEventListener('click', () => {
stored.clear();
renderGrid([]);
document.getElementById('trace').textContent = '';
document.getElementById('result').textContent = '';
});
// Seed with a few initial values for a nice first look
[5, 12, 23, 31, 40, 47, 58].forEach(k => stored.add(k));
renderGrid([]);
Comments
Loading comments...