<p class="hint">Type a short string (up to 15 chars) and press <b>Build</b>. The automaton is constructed character by character; then a single linear pass counts every distinct non-empty substring — no brute force needed.</p>
<div class="controls">
<input id="inp" type="text" maxlength="15" value="abcbc" spellcheck="false" autocomplete="off" />
<button id="build" type="button">Build</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div id="stats" class="stats"></div>
<div id="vis" class="vis"></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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
input { font: 600 15px ui-monospace, monospace; padding: .4rem .7rem;
border: 1.5px solid #adb1b8; border-radius: 8px; width: 11rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .95rem; min-height: 1.4em; margin-bottom: .5rem; }
.stats b { color: #1d3557; }
.vis { overflow-x: auto; padding-bottom: .4rem; }
svg.aut { display: block; }
.log { font-size: .82rem; color: #555; margin-top: .6rem; line-height: 1.6;
max-height: 90px; overflow-y: auto; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Suffix Automaton (DAWG) – online construction in O(n) time.
// Each state stores:
// len – length of the longest string in its endpos class
// link – suffix link (parent in the suffix link tree)
// next – transition map { char -> state_id }
// After building, distinct substrings = sum over states (excluding initial) of (state.len - parent.len), where parent = states[state.link].
function buildSA(s) {
const states = [{ len: 0, link: -1, next: {} }]; // state 0 = initial
let last = 0;
for (const ch of s) {
const cur = states.length;
states.push({ len: states[last].len + 1, link: -1, next: {} });
let p = last;
while (p !== -1 && !states[p].next[ch]) {
states[p].next[ch] = cur;
p = states[p].link;
}
if (p === -1) {
states[cur].link = 0;
} else {
const q = states[p].next[ch];
if (states[p].len + 1 === states[q].len) {
states[cur].link = q;
} else {
const clone = states.length;
states.push({ len: states[p].len + 1, link: states[q].link, next: { ...states[q].next } });
while (p !== -1 && states[p].next[ch] === q) {
states[p].next[ch] = clone;
p = states[p].link;
}
states[q].link = clone;
states[cur].link = clone;
}
}
last = cur;
}
return states;
}
function countDistinct(states) {
let total = 0;
for (let i = 1; i < states.length; i++) {
const parentLen = states[i].link === -1 ? 0 : states[states[i].link].len;
total += states[i].len - parentLen;
}
return total;
}
// Brute-force verification: enumerate all substrings and count distinct ones.
function bruteForceCount(s) {
const seen = new Set();
for (let i = 0; i < s.length; i++)
for (let j = i + 1; j <= s.length; j++)
seen.add(s.slice(i, j));
return seen.size;
}
// Simple layered layout: BFS from state 0.
function layout(states) {
const W = 420, H = 200, R = 17;
const layers = [];
const depth = new Array(states.length).fill(-1);
depth[0] = 0;
const queue = [0];
while (queue.length) {
const u = queue.shift();
for (const v of Object.values(states[u].next)) {
if (depth[v] === -1) { depth[v] = depth[u] + 1; queue.push(v); }
}
}
const maxD = Math.max(...depth);
const byLayer = [];
for (let d = 0; d <= maxD; d++) byLayer.push([]);
depth.forEach((d, i) => byLayer[d].push(i));
const pos = new Array(states.length);
const colW = maxD > 0 ? Math.min(80, (W - 2 * R) / maxD) : 80;
byLayer.forEach((layer, d) => {
const rowH = H / (layer.length + 1);
layer.forEach((id, i) => {
pos[id] = { x: R + d * colW, y: rowH * (i + 1) };
});
});
return { pos, W, H: Math.max(H, byLayer.reduce((m, l) => Math.max(m, l.length), 0) * 36 + 20), R, maxD };
}
function renderSVG(states, s) {
const { pos, W, H, R } = layout(states);
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('class', 'aut');
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
svg.setAttribute('width', W);
svg.setAttribute('height', H);
// Arrowhead marker
const defs = document.createElementNS(ns, 'defs');
const marker = document.createElementNS(ns, 'marker');
marker.setAttribute('id', 'arr');
marker.setAttribute('markerWidth', '6');
marker.setAttribute('markerHeight', '6');
marker.setAttribute('refX', '5');
marker.setAttribute('refY', '3');
marker.setAttribute('orient', 'auto');
const poly = document.createElementNS(ns, 'polygon');
poly.setAttribute('points', '0 0 6 3 0 6');
poly.setAttribute('fill', '#1d3557');
marker.appendChild(poly);
defs.appendChild(marker);
svg.appendChild(defs);
// Suffix links (dashed)
for (let i = 1; i < states.length; i++) {
const lk = states[i].link;
if (lk < 0) continue;
const { x: x1, y: y1 } = pos[i];
const { x: x2, y: y2 } = pos[lk];
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', '#adb1b8');
line.setAttribute('stroke-width', '1');
line.setAttribute('stroke-dasharray', '3 3');
svg.appendChild(line);
}
// Transitions (solid)
for (let u = 0; u < states.length; u++) {
for (const [ch, v] of Object.entries(states[u].next)) {
const { x: x1, y: y1 } = pos[u];
const { x: x2, y: y2 } = pos[v];
const dx = x2 - x1, dy = y2 - y1, dist = Math.hypot(dx, dy);
if (dist === 0) continue;
const ex = dx / dist, ey = dy / dist;
const sx = x1 + ex * R, sy = y1 + ey * R;
const tx = x2 - ex * (R + 5), ty = y2 - ey * (R + 5);
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', sx); line.setAttribute('y1', sy);
line.setAttribute('x2', tx); line.setAttribute('y2', ty);
line.setAttribute('stroke', '#1d3557');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('marker-end', 'url(#arr)');
svg.appendChild(line);
// Label
const mx = (sx + tx) / 2, my = (sy + ty) / 2;
const lbl = document.createElementNS(ns, 'text');
lbl.setAttribute('x', mx);
lbl.setAttribute('y', my - 3);
lbl.setAttribute('font-size', '10');
lbl.setAttribute('fill', '#333');
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('font-family', 'ui-monospace, monospace');
lbl.textContent = ch;
svg.appendChild(lbl);
}
}
// States (circles)
for (let i = 0; i < states.length; i++) {
const { x, y } = pos[i];
const circle = document.createElementNS(ns, 'circle');
circle.setAttribute('cx', x); circle.setAttribute('cy', y);
circle.setAttribute('r', R);
circle.setAttribute('fill', i === 0 ? '#e8eef3' : '#d6e4f0');
circle.setAttribute('stroke', '#1d3557');
circle.setAttribute('stroke-width', '1.5');
svg.appendChild(circle);
const t = document.createElementNS(ns, 'text');
t.setAttribute('x', x); t.setAttribute('y', y + 4);
t.setAttribute('font-size', '11');
t.setAttribute('fill', '#1d3557');
t.setAttribute('text-anchor', 'middle');
t.setAttribute('font-family', 'ui-monospace, monospace');
t.setAttribute('font-weight', '700');
t.textContent = i;
svg.appendChild(t);
}
return svg;
}
function run() {
const s = document.getElementById('inp').value.replace(/\s/g, '');
if (!s) { document.getElementById('stats').innerHTML = ''; document.getElementById('vis').innerHTML = ''; document.getElementById('log').textContent = ''; return; }
const states = buildSA(s);
const distinct = countDistinct(states);
const bf = bruteForceCount(s);
const maxStates = 2 * s.length - 1;
document.getElementById('stats').innerHTML =
fmt('String: <b>%s%</b> (length %length%) | States: <b>%length2%</b> / max %maxStates% | Distinct substrings: <b>%distinct%</b>', { s: s, length: s.length, length2: states.length, maxStates: maxStates, distinct: distinct });
const vis = document.getElementById('vis');
vis.innerHTML = '';
vis.appendChild(renderSVG(states, s));
const match = distinct === bf ? '✓ matches brute force' : '✗ mismatch!';
document.getElementById('log').textContent =
fmt('Brute-force distinct count: %bf% — %match%. Suffix links shown as dashed gray lines. Solid blue arrows = transitions (labeled with character).', { bf: bf, match: match });
console.log('SA states:', states.length, '| distinct substrings (SA):', distinct, '| brute force:', bf, '| match:', distinct === bf);
}
document.getElementById('build').addEventListener('click', run);
document.getElementById('inp').addEventListener('keydown', e => { if (e.key === 'Enter') run(); });
document.getElementById('reset').addEventListener('click', () => {
document.getElementById('inp').value = '';
document.getElementById('stats').innerHTML = '';
document.getElementById('vis').innerHTML = '';
document.getElementById('log').textContent = '';
});
run();
<p class="hint">Type a short string (up to 15 chars) and press <b>Build</b>. The automaton is constructed character by character; then a single linear pass counts every distinct non-empty substring — no brute force needed.</p>
<div class="controls">
<input id="inp" type="text" maxlength="15" value="abcbc" spellcheck="false" autocomplete="off" />
<button id="build" type="button">Build</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div id="stats" class="stats"></div>
<div id="vis" class="vis"></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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
input { font: 600 15px ui-monospace, monospace; padding: .4rem .7rem;
border: 1.5px solid #adb1b8; border-radius: 8px; width: 11rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .95rem; min-height: 1.4em; margin-bottom: .5rem; }
.stats b { color: #1d3557; }
.vis { overflow-x: auto; padding-bottom: .4rem; }
svg.aut { display: block; }
.log { font-size: .82rem; color: #555; margin-top: .6rem; line-height: 1.6;
max-height: 90px; overflow-y: auto; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Suffix Automaton (DAWG) – online construction in O(n) time.
// Each state stores:
// len – length of the longest string in its endpos class
// link – suffix link (parent in the suffix link tree)
// next – transition map { char -> state_id }
// After building, distinct substrings = sum over states (excluding initial) of (state.len - parent.len), where parent = states[state.link].
function buildSA(s) {
const states = [{ len: 0, link: -1, next: {} }]; // state 0 = initial
let last = 0;
for (const ch of s) {
const cur = states.length;
states.push({ len: states[last].len + 1, link: -1, next: {} });
let p = last;
while (p !== -1 && !states[p].next[ch]) {
states[p].next[ch] = cur;
p = states[p].link;
}
if (p === -1) {
states[cur].link = 0;
} else {
const q = states[p].next[ch];
if (states[p].len + 1 === states[q].len) {
states[cur].link = q;
} else {
const clone = states.length;
states.push({ len: states[p].len + 1, link: states[q].link, next: { ...states[q].next } });
while (p !== -1 && states[p].next[ch] === q) {
states[p].next[ch] = clone;
p = states[p].link;
}
states[q].link = clone;
states[cur].link = clone;
}
}
last = cur;
}
return states;
}
function countDistinct(states) {
let total = 0;
for (let i = 1; i < states.length; i++) {
const parentLen = states[i].link === -1 ? 0 : states[states[i].link].len;
total += states[i].len - parentLen;
}
return total;
}
// Brute-force verification: enumerate all substrings and count distinct ones.
function bruteForceCount(s) {
const seen = new Set();
for (let i = 0; i < s.length; i++)
for (let j = i + 1; j <= s.length; j++)
seen.add(s.slice(i, j));
return seen.size;
}
// Simple layered layout: BFS from state 0.
function layout(states) {
const W = 420, H = 200, R = 17;
const layers = [];
const depth = new Array(states.length).fill(-1);
depth[0] = 0;
const queue = [0];
while (queue.length) {
const u = queue.shift();
for (const v of Object.values(states[u].next)) {
if (depth[v] === -1) { depth[v] = depth[u] + 1; queue.push(v); }
}
}
const maxD = Math.max(...depth);
const byLayer = [];
for (let d = 0; d <= maxD; d++) byLayer.push([]);
depth.forEach((d, i) => byLayer[d].push(i));
const pos = new Array(states.length);
const colW = maxD > 0 ? Math.min(80, (W - 2 * R) / maxD) : 80;
byLayer.forEach((layer, d) => {
const rowH = H / (layer.length + 1);
layer.forEach((id, i) => {
pos[id] = { x: R + d * colW, y: rowH * (i + 1) };
});
});
return { pos, W, H: Math.max(H, byLayer.reduce((m, l) => Math.max(m, l.length), 0) * 36 + 20), R, maxD };
}
function renderSVG(states, s) {
const { pos, W, H, R } = layout(states);
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('class', 'aut');
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
svg.setAttribute('width', W);
svg.setAttribute('height', H);
// Arrowhead marker
const defs = document.createElementNS(ns, 'defs');
const marker = document.createElementNS(ns, 'marker');
marker.setAttribute('id', 'arr');
marker.setAttribute('markerWidth', '6');
marker.setAttribute('markerHeight', '6');
marker.setAttribute('refX', '5');
marker.setAttribute('refY', '3');
marker.setAttribute('orient', 'auto');
const poly = document.createElementNS(ns, 'polygon');
poly.setAttribute('points', '0 0 6 3 0 6');
poly.setAttribute('fill', '#1d3557');
marker.appendChild(poly);
defs.appendChild(marker);
svg.appendChild(defs);
// Suffix links (dashed)
for (let i = 1; i < states.length; i++) {
const lk = states[i].link;
if (lk < 0) continue;
const { x: x1, y: y1 } = pos[i];
const { x: x2, y: y2 } = pos[lk];
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
line.setAttribute('stroke', '#adb1b8');
line.setAttribute('stroke-width', '1');
line.setAttribute('stroke-dasharray', '3 3');
svg.appendChild(line);
}
// Transitions (solid)
for (let u = 0; u < states.length; u++) {
for (const [ch, v] of Object.entries(states[u].next)) {
const { x: x1, y: y1 } = pos[u];
const { x: x2, y: y2 } = pos[v];
const dx = x2 - x1, dy = y2 - y1, dist = Math.hypot(dx, dy);
if (dist === 0) continue;
const ex = dx / dist, ey = dy / dist;
const sx = x1 + ex * R, sy = y1 + ey * R;
const tx = x2 - ex * (R + 5), ty = y2 - ey * (R + 5);
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', sx); line.setAttribute('y1', sy);
line.setAttribute('x2', tx); line.setAttribute('y2', ty);
line.setAttribute('stroke', '#1d3557');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('marker-end', 'url(#arr)');
svg.appendChild(line);
// Label
const mx = (sx + tx) / 2, my = (sy + ty) / 2;
const lbl = document.createElementNS(ns, 'text');
lbl.setAttribute('x', mx);
lbl.setAttribute('y', my - 3);
lbl.setAttribute('font-size', '10');
lbl.setAttribute('fill', '#333');
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('font-family', 'ui-monospace, monospace');
lbl.textContent = ch;
svg.appendChild(lbl);
}
}
// States (circles)
for (let i = 0; i < states.length; i++) {
const { x, y } = pos[i];
const circle = document.createElementNS(ns, 'circle');
circle.setAttribute('cx', x); circle.setAttribute('cy', y);
circle.setAttribute('r', R);
circle.setAttribute('fill', i === 0 ? '#e8eef3' : '#d6e4f0');
circle.setAttribute('stroke', '#1d3557');
circle.setAttribute('stroke-width', '1.5');
svg.appendChild(circle);
const t = document.createElementNS(ns, 'text');
t.setAttribute('x', x); t.setAttribute('y', y + 4);
t.setAttribute('font-size', '11');
t.setAttribute('fill', '#1d3557');
t.setAttribute('text-anchor', 'middle');
t.setAttribute('font-family', 'ui-monospace, monospace');
t.setAttribute('font-weight', '700');
t.textContent = i;
svg.appendChild(t);
}
return svg;
}
function run() {
const s = document.getElementById('inp').value.replace(/\s/g, '');
if (!s) { document.getElementById('stats').innerHTML = ''; document.getElementById('vis').innerHTML = ''; document.getElementById('log').textContent = ''; return; }
const states = buildSA(s);
const distinct = countDistinct(states);
const bf = bruteForceCount(s);
const maxStates = 2 * s.length - 1;
document.getElementById('stats').innerHTML =
fmt('String: <b>%s%</b> (length %length%) | States: <b>%length2%</b> / max %maxStates% | Distinct substrings: <b>%distinct%</b>', { s: s, length: s.length, length2: states.length, maxStates: maxStates, distinct: distinct });
const vis = document.getElementById('vis');
vis.innerHTML = '';
vis.appendChild(renderSVG(states, s));
const match = distinct === bf ? '✓ matches brute force' : '✗ mismatch!';
document.getElementById('log').textContent =
fmt('Brute-force distinct count: %bf% — %match%. Suffix links shown as dashed gray lines. Solid blue arrows = transitions (labeled with character).', { bf: bf, match: match });
console.log('SA states:', states.length, '| distinct substrings (SA):', distinct, '| brute force:', bf, '| match:', distinct === bf);
}
document.getElementById('build').addEventListener('click', run);
document.getElementById('inp').addEventListener('keydown', e => { if (e.key === 'Enter') run(); });
document.getElementById('reset').addEventListener('click', () => {
document.getElementById('inp').value = '';
document.getElementById('stats').innerHTML = '';
document.getElementById('vis').innerHTML = '';
document.getElementById('log').textContent = '';
});
run();
Notice two things. First, the number of states grows at most linearly — never more than 2n−1 for a string of length n. Second, the distinct-substring count is computed in one linear pass over the automaton; brute-force enumeration would take O(n2) time just to list them. That gap — linear automaton vs quadratic enumeration — is the suffix automaton's superpower.
Comments
Loading comments...