<p class="hint">Select a <b>source</b> and a <b>target</b> node, then click <b>Step</b> to run the inductive counting algorithm one round at a time. The algorithm counts reachable vertices layer-by-layer using only two counters — no explicit visited list — and determines whether the target is reachable.</p>
<div class="graph-wrap">
<svg id="graph-svg" viewBox="0 0 340 220" width="340" height="220"></svg>
</div>
<div class="controls">
<label>Source <select id="sel-source"></select></label>
<label>Target <select id="sel-target"></select></label>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="memory-box">
<span class="mem-label">Memory (logspace):</span>
<span id="mem-round">round = –</span>
<span id="mem-prev">c_prev = –</span>
<span id="mem-curr">c_curr = –</span>
</div>
<div id="log" class="log"></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; }
.graph-wrap { display: flex; justify-content: center; margin-bottom: .5rem; }
svg { border: 1px solid #dde3ea; border-radius: 10px; background: #f7f9fb; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .9rem; }
select { font-size: .9rem; border: 1px solid #bbb; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.memory-box { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
background: #eef3f8; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .4rem .75rem; font-size: .85rem; margin-bottom: .5rem; }
.mem-label { font-weight: 700; color: #1d3557; }
.memory-box span { background: #fff; border: 1px solid #cdd9e3; border-radius: 5px;
padding: .1rem .45rem; font-family: ui-monospace, monospace; }
.memory-box .mem-label { background: none; border: none; }
.log { font-size: .82rem; color: #333; max-height: 110px; overflow-y: auto;
border: 1px solid #e0e6ec; border-radius: 8px; padding: .5rem .75rem;
background: #fff; line-height: 1.55; }
.log .ok { color: #0a7d33; font-weight: 600; }
.log .bad { color: #c92f3c; font-weight: 600; }
.log .info { color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Immerman-Szelepcsényi inductive counting demo.
// Graph: 7 nodes, directed edges. Algorithm decides reachability of target t from source s using only two O(log n) counters — no visited list.
//
// At round d (d = 0..N-1):
// c_prev = number of nodes reachable from s in <= d steps (known from prev round)
// c_curr = count being built for d+1
// For each node v, guess (simulate deterministically) whether it is reachable in <= d+1 steps by BFS up to depth d+1 (stand-in for nondeterministic guess).
// If yes, increment c_curr.
// After all nodes processed, c_prev = c_curr, advance d.
// At the end, if target was reached at any round => reachable; else => NOT reachable.
const NODES = [
{ id: 0, label: 'A', x: 40, y: 110 },
{ id: 1, label: 'B', x: 120, y: 40 },
{ id: 2, label: 'C', x: 120, y: 180 },
{ id: 3, label: 'D', x: 200, y: 110 },
{ id: 4, label: 'E', x: 280, y: 40 },
{ id: 5, label: 'F', x: 280, y: 180 },
{ id: 6, label: 'G', x: 300, y: 110 },
];
// Directed adjacency list
const ADJ = {
0: [1, 2],
1: [3],
2: [3],
3: [4, 5],
4: [6],
5: [],
6: [],
};
const N = NODES.length;
// BFS distance from src to all nodes (deterministic simulation of ND guesses)
function bfsDistances(src) {
const dist = new Array(N).fill(Infinity);
dist[src] = 0;
const q = [src];
while (q.length) {
const u = q.shift();
for (const v of ADJ[u]) {
if (dist[v] === Infinity) { dist[v] = dist[u] + 1; q.push(v); }
}
}
return dist;
}
let src = 0, tgt = 6;
let dist = bfsDistances(src);
let round = -1; // -1 = not started
let cPrev = 1; // c_0 = 1 (only src is reachable at d=0)
let reachableAtRound = []; // reachableAtRound[d] = set of node ids reachable in <=d steps
let targetFound = false;
let done = false;
// Build reachableAtRound for animation
function buildRounds(src) {
const d = bfsDistances(src);
reachableAtRound = [];
for (let step = 0; step < N; step++) {
reachableAtRound.push(new Set(NODES.filter(n => d[n.id] <= step).map(n => n.id)));
}
}
// --- SVG rendering ---
const svg = document.getElementById('graph-svg');
function arrowId(u, v) { return `arrow-${u}-${v}`; }
function renderGraph(highlightSet, highlightEdges) {
svg.innerHTML = '';
// Defs for arrowhead
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.innerHTML = `<marker id="arr" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#6b8299"/></marker>
<marker id="arr-hi" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#1d3557"/></marker>`;
svg.appendChild(defs);
// Draw edges
for (const [u, neighbors] of Object.entries(ADJ)) {
const uid = parseInt(u);
for (const vid of neighbors) {
const a = NODES[uid], b = NODES[vid];
const dx = b.x - a.x, dy = b.y - a.y;
const len = Math.sqrt(dx*dx + dy*dy);
const nx = dx/len, ny = dy/len;
const R = 18;
const x1 = a.x + nx*R, y1 = a.y + ny*R;
const x2 = b.x - nx*(R+4), y2 = b.y - ny*(R+4);
const hi = highlightEdges && highlightEdges.has(`${uid}-${vid}`);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', hi ? '#1d3557' : '#6b8299');
line.setAttribute('stroke-width', hi ? '2' : '1.4');
line.setAttribute('marker-end', hi ? 'url(#arr-hi)' : 'url(#arr)');
svg.appendChild(line);
}
}
// Draw nodes
for (const node of NODES) {
const inSet = highlightSet && highlightSet.has(node.id);
const isSrc = node.id === src;
const isTgt = node.id === tgt;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const circ = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circ.setAttribute('cx', node.x); circ.setAttribute('cy', node.y); circ.setAttribute('r', 18);
circ.setAttribute('fill', inSet ? '#1d3557' : '#e8eef3');
circ.setAttribute('stroke', isSrc ? '#e63946' : (isTgt ? '#e9c46a' : '#adb7c4'));
circ.setAttribute('stroke-width', (isSrc || isTgt) ? '3' : '1.5');
g.appendChild(circ);
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt.setAttribute('x', node.x); txt.setAttribute('y', node.y + 5);
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('font-size', '14'); txt.setAttribute('font-weight', '700');
txt.setAttribute('fill', inSet ? '#fff' : '#1d3557');
txt.textContent = node.label;
g.appendChild(txt);
// Labels src/tgt
if (isSrc || isTgt) {
const lbl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
lbl.setAttribute('x', node.x); lbl.setAttribute('y', node.y - 23);
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('font-size', '10'); lbl.setAttribute('font-weight', '600');
lbl.setAttribute('fill', isSrc ? '#e63946' : '#b5860a');
lbl.textContent = isSrc ? 'src' : 'tgt';
g.appendChild(lbl);
}
svg.appendChild(g);
}
}
function log(msg, cls) {
const div = document.getElementById('log');
const p = document.createElement('p');
p.style.margin = '0';
if (cls) p.className = cls;
p.innerHTML = msg;
div.appendChild(p);
div.scrollTop = div.scrollHeight;
}
function updateMemory(r, prev, curr) {
document.getElementById('mem-round').textContent = fmt('round = %v%', { v: r < 0 ? '–' : r });
document.getElementById('mem-prev').textContent = fmt('c_prev = %v%', { v: prev < 0 ? '–' : prev });
document.getElementById('mem-curr').textContent = fmt('c_curr = %v%', { v: curr < 0 ? '–' : curr });
}
function resetState() {
src = parseInt(document.getElementById('sel-source').value);
tgt = parseInt(document.getElementById('sel-target').value);
dist = bfsDistances(src);
buildRounds(src);
round = -1; cPrev = 1; targetFound = false; done = false;
document.getElementById('log').innerHTML = '';
updateMemory(-1, -1, -1);
renderGraph(new Set([src]), null);
log(fmt('Source: <b>%label%</b>, Target: <b>%label2%</b>. Ready to count.', { label: NODES[src].label, label2: NODES[tgt].label }), 'info');
log(`Round 0 initialised: c_prev = 1 (only source is reachable at distance 0).`, 'info');
}
function step() {
if (done) { log('Algorithm complete. Press Reset to try again.', 'info'); return; }
round++;
if (round >= N - 1) {
// Final verdict
done = true;
if (targetFound) {
log(fmt('<b>Result: %label% IS reachable from %label2%.</b>', { label: NODES[tgt].label, label2: NODES[src].label }), 'ok');
} else {
log(fmt('<b>Result: %label% is NOT reachable from %label2%.</b>', { label: NODES[tgt].label, label2: NODES[src].label }), 'bad');
}
document.getElementById('btn-step').disabled = true;
renderGraph(reachableAtRound[round] || new Set(), null);
return;
}
// Count reachable at round+1 (i.e., dist <= round+1)
const reachNow = reachableAtRound[round + 1];
const cCurr = reachNow.size;
if (reachNow.has(tgt) && !targetFound) {
targetFound = true;
log(" " + fmt('→ Target <b>%label%</b> confirmed reachable at distance %v%!', { label: NODES[tgt].label, v: round+1 }), 'ok');
}
// Build highlighted edges: edges u→v where v is newly reachable this round
const prevReach = reachableAtRound[round];
const hiEdges = new Set();
for (const u of prevReach) {
for (const v of ADJ[u]) {
if (!prevReach.has(v) && reachNow.has(v)) hiEdges.add(`${u}-${v}`);
}
}
updateMemory(round + 1, cPrev, cCurr);
log(fmt('Round %v%: c_prev=%cPrev% → scanning all nodes → c_curr=%cCurr% (reachable in ≤%v% steps). Memory: 2 counters only.', { v: round+1, cPrev: cPrev, cCurr: cCurr }), 'info');
cPrev = cCurr;
renderGraph(reachNow, hiEdges);
}
// Populate dropdowns
function populateSelects() {
['sel-source', 'sel-target'].forEach((id, idx) => {
const sel = document.getElementById(id);
sel.innerHTML = '';
NODES.forEach(n => {
const opt = document.createElement('option');
opt.value = n.id; opt.textContent = n.label;
sel.appendChild(opt);
});
sel.value = idx === 0 ? 0 : 6;
});
}
populateSelects();
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-reset').addEventListener('click', resetState);
document.getElementById('sel-source').addEventListener('change', resetState);
document.getElementById('sel-target').addEventListener('change', resetState);
resetState();
<p class="hint">Select a <b>source</b> and a <b>target</b> node, then click <b>Step</b> to run the inductive counting algorithm one round at a time. The algorithm counts reachable vertices layer-by-layer using only two counters — no explicit visited list — and determines whether the target is reachable.</p>
<div class="graph-wrap">
<svg id="graph-svg" viewBox="0 0 340 220" width="340" height="220"></svg>
</div>
<div class="controls">
<label>Source <select id="sel-source"></select></label>
<label>Target <select id="sel-target"></select></label>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="memory-box">
<span class="mem-label">Memory (logspace):</span>
<span id="mem-round">round = –</span>
<span id="mem-prev">c_prev = –</span>
<span id="mem-curr">c_curr = –</span>
</div>
<div id="log" class="log"></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; }
.graph-wrap { display: flex; justify-content: center; margin-bottom: .5rem; }
svg { border: 1px solid #dde3ea; border-radius: 10px; background: #f7f9fb; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .9rem; }
select { font-size: .9rem; border: 1px solid #bbb; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.memory-box { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
background: #eef3f8; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .4rem .75rem; font-size: .85rem; margin-bottom: .5rem; }
.mem-label { font-weight: 700; color: #1d3557; }
.memory-box span { background: #fff; border: 1px solid #cdd9e3; border-radius: 5px;
padding: .1rem .45rem; font-family: ui-monospace, monospace; }
.memory-box .mem-label { background: none; border: none; }
.log { font-size: .82rem; color: #333; max-height: 110px; overflow-y: auto;
border: 1px solid #e0e6ec; border-radius: 8px; padding: .5rem .75rem;
background: #fff; line-height: 1.55; }
.log .ok { color: #0a7d33; font-weight: 600; }
.log .bad { color: #c92f3c; font-weight: 600; }
.log .info { color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Immerman-Szelepcsényi inductive counting demo.
// Graph: 7 nodes, directed edges. Algorithm decides reachability of target t from source s using only two O(log n) counters — no visited list.
//
// At round d (d = 0..N-1):
// c_prev = number of nodes reachable from s in <= d steps (known from prev round)
// c_curr = count being built for d+1
// For each node v, guess (simulate deterministically) whether it is reachable in <= d+1 steps by BFS up to depth d+1 (stand-in for nondeterministic guess).
// If yes, increment c_curr.
// After all nodes processed, c_prev = c_curr, advance d.
// At the end, if target was reached at any round => reachable; else => NOT reachable.
const NODES = [
{ id: 0, label: 'A', x: 40, y: 110 },
{ id: 1, label: 'B', x: 120, y: 40 },
{ id: 2, label: 'C', x: 120, y: 180 },
{ id: 3, label: 'D', x: 200, y: 110 },
{ id: 4, label: 'E', x: 280, y: 40 },
{ id: 5, label: 'F', x: 280, y: 180 },
{ id: 6, label: 'G', x: 300, y: 110 },
];
// Directed adjacency list
const ADJ = {
0: [1, 2],
1: [3],
2: [3],
3: [4, 5],
4: [6],
5: [],
6: [],
};
const N = NODES.length;
// BFS distance from src to all nodes (deterministic simulation of ND guesses)
function bfsDistances(src) {
const dist = new Array(N).fill(Infinity);
dist[src] = 0;
const q = [src];
while (q.length) {
const u = q.shift();
for (const v of ADJ[u]) {
if (dist[v] === Infinity) { dist[v] = dist[u] + 1; q.push(v); }
}
}
return dist;
}
let src = 0, tgt = 6;
let dist = bfsDistances(src);
let round = -1; // -1 = not started
let cPrev = 1; // c_0 = 1 (only src is reachable at d=0)
let reachableAtRound = []; // reachableAtRound[d] = set of node ids reachable in <=d steps
let targetFound = false;
let done = false;
// Build reachableAtRound for animation
function buildRounds(src) {
const d = bfsDistances(src);
reachableAtRound = [];
for (let step = 0; step < N; step++) {
reachableAtRound.push(new Set(NODES.filter(n => d[n.id] <= step).map(n => n.id)));
}
}
// --- SVG rendering ---
const svg = document.getElementById('graph-svg');
function arrowId(u, v) { return `arrow-${u}-${v}`; }
function renderGraph(highlightSet, highlightEdges) {
svg.innerHTML = '';
// Defs for arrowhead
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.innerHTML = `<marker id="arr" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#6b8299"/></marker>
<marker id="arr-hi" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#1d3557"/></marker>`;
svg.appendChild(defs);
// Draw edges
for (const [u, neighbors] of Object.entries(ADJ)) {
const uid = parseInt(u);
for (const vid of neighbors) {
const a = NODES[uid], b = NODES[vid];
const dx = b.x - a.x, dy = b.y - a.y;
const len = Math.sqrt(dx*dx + dy*dy);
const nx = dx/len, ny = dy/len;
const R = 18;
const x1 = a.x + nx*R, y1 = a.y + ny*R;
const x2 = b.x - nx*(R+4), y2 = b.y - ny*(R+4);
const hi = highlightEdges && highlightEdges.has(`${uid}-${vid}`);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', hi ? '#1d3557' : '#6b8299');
line.setAttribute('stroke-width', hi ? '2' : '1.4');
line.setAttribute('marker-end', hi ? 'url(#arr-hi)' : 'url(#arr)');
svg.appendChild(line);
}
}
// Draw nodes
for (const node of NODES) {
const inSet = highlightSet && highlightSet.has(node.id);
const isSrc = node.id === src;
const isTgt = node.id === tgt;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const circ = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circ.setAttribute('cx', node.x); circ.setAttribute('cy', node.y); circ.setAttribute('r', 18);
circ.setAttribute('fill', inSet ? '#1d3557' : '#e8eef3');
circ.setAttribute('stroke', isSrc ? '#e63946' : (isTgt ? '#e9c46a' : '#adb7c4'));
circ.setAttribute('stroke-width', (isSrc || isTgt) ? '3' : '1.5');
g.appendChild(circ);
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt.setAttribute('x', node.x); txt.setAttribute('y', node.y + 5);
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('font-size', '14'); txt.setAttribute('font-weight', '700');
txt.setAttribute('fill', inSet ? '#fff' : '#1d3557');
txt.textContent = node.label;
g.appendChild(txt);
// Labels src/tgt
if (isSrc || isTgt) {
const lbl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
lbl.setAttribute('x', node.x); lbl.setAttribute('y', node.y - 23);
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('font-size', '10'); lbl.setAttribute('font-weight', '600');
lbl.setAttribute('fill', isSrc ? '#e63946' : '#b5860a');
lbl.textContent = isSrc ? 'src' : 'tgt';
g.appendChild(lbl);
}
svg.appendChild(g);
}
}
function log(msg, cls) {
const div = document.getElementById('log');
const p = document.createElement('p');
p.style.margin = '0';
if (cls) p.className = cls;
p.innerHTML = msg;
div.appendChild(p);
div.scrollTop = div.scrollHeight;
}
function updateMemory(r, prev, curr) {
document.getElementById('mem-round').textContent = fmt('round = %v%', { v: r < 0 ? '–' : r });
document.getElementById('mem-prev').textContent = fmt('c_prev = %v%', { v: prev < 0 ? '–' : prev });
document.getElementById('mem-curr').textContent = fmt('c_curr = %v%', { v: curr < 0 ? '–' : curr });
}
function resetState() {
src = parseInt(document.getElementById('sel-source').value);
tgt = parseInt(document.getElementById('sel-target').value);
dist = bfsDistances(src);
buildRounds(src);
round = -1; cPrev = 1; targetFound = false; done = false;
document.getElementById('log').innerHTML = '';
updateMemory(-1, -1, -1);
renderGraph(new Set([src]), null);
log(fmt('Source: <b>%label%</b>, Target: <b>%label2%</b>. Ready to count.', { label: NODES[src].label, label2: NODES[tgt].label }), 'info');
log(`Round 0 initialised: c_prev = 1 (only source is reachable at distance 0).`, 'info');
}
function step() {
if (done) { log('Algorithm complete. Press Reset to try again.', 'info'); return; }
round++;
if (round >= N - 1) {
// Final verdict
done = true;
if (targetFound) {
log(fmt('<b>Result: %label% IS reachable from %label2%.</b>', { label: NODES[tgt].label, label2: NODES[src].label }), 'ok');
} else {
log(fmt('<b>Result: %label% is NOT reachable from %label2%.</b>', { label: NODES[tgt].label, label2: NODES[src].label }), 'bad');
}
document.getElementById('btn-step').disabled = true;
renderGraph(reachableAtRound[round] || new Set(), null);
return;
}
// Count reachable at round+1 (i.e., dist <= round+1)
const reachNow = reachableAtRound[round + 1];
const cCurr = reachNow.size;
if (reachNow.has(tgt) && !targetFound) {
targetFound = true;
log(" " + fmt('→ Target <b>%label%</b> confirmed reachable at distance %v%!', { label: NODES[tgt].label, v: round+1 }), 'ok');
}
// Build highlighted edges: edges u→v where v is newly reachable this round
const prevReach = reachableAtRound[round];
const hiEdges = new Set();
for (const u of prevReach) {
for (const v of ADJ[u]) {
if (!prevReach.has(v) && reachNow.has(v)) hiEdges.add(`${u}-${v}`);
}
}
updateMemory(round + 1, cPrev, cCurr);
log(fmt('Round %v%: c_prev=%cPrev% → scanning all nodes → c_curr=%cCurr% (reachable in ≤%v% steps). Memory: 2 counters only.', { v: round+1, cPrev: cPrev, cCurr: cCurr }), 'info');
cPrev = cCurr;
renderGraph(reachNow, hiEdges);
}
// Populate dropdowns
function populateSelects() {
['sel-source', 'sel-target'].forEach((id, idx) => {
const sel = document.getElementById(id);
sel.innerHTML = '';
NODES.forEach(n => {
const opt = document.createElement('option');
opt.value = n.id; opt.textContent = n.label;
sel.appendChild(opt);
});
sel.value = idx === 0 ? 0 : 6;
});
}
populateSelects();
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-reset').addEventListener('click', resetState);
document.getElementById('sel-source').addEventListener('change', resetState);
document.getElementById('sel-target').addEventListener('change', resetState);
resetState();
Pick any source and target in the graph, then step through the algorithm. At each round the demo counts reachable vertices by distance, keeping only two small numbers in memory (the previous count and the current counter). At the end it declares whether t is reachable — or provably not reachable — using only O(logn) bits of bookkeeping.
Comments
Loading comments...