Type two strings and set a threshold k. The table below shows the DP cells that Ukkonen's banded algorithm actually computes (colored) versus the cells a naive full computation would fill (gray outlines). Cells outside the band are skipped.
<!-- Banded edit-distance demo — DP table with diagonal band highlighted -->
<div class="controls">
<div class="field">
<label for="strA">String A</label>
<input id="strA" type="text" value="kitten" maxlength="14" spellcheck="false" placeholder="first string" />
</div>
<div class="field">
<label for="strB">String B</label>
<input id="strB" type="text" value="sitting" maxlength="14" spellcheck="false" placeholder="second string" />
</div>
<div class="field field-k">
<label for="kVal">Threshold k = <span id="kDisplay">3</span></label>
<input id="kVal" type="range" min="0" max="14" value="3" />
</div>
</div>
<div id="result" class="result"></div>
<div id="legend" class="legend">
<span class="leg-band">In band (computed)</span>
<span class="leg-skip">Outside band (skipped)</span>
<span class="leg-match">Final answer</span>
</div>
<div id="tableWrap" class="table-wrap"></div>
/* Styles for the banded edit-distance demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .7rem; }
.field { display: flex; flex-direction: column; gap: .2rem; }
.field label { font-size: .8rem; font-weight: 600; color: #555; }
.field input[type=text] { width: 9rem; padding: .3rem .5rem; border: 1px solid #bbb;
border-radius: 6px; font-size: .95rem; font-family: ui-monospace, monospace; }
.field-k label { display: flex; align-items: center; gap: .3rem; }
.field-k input { width: 9rem; cursor: pointer; }
.result { font-weight: 700; font-size: 1rem; min-height: 1.4em; margin-bottom: .4rem; }
.result.exact { color: #0a7d33; }
.result.exceeds { color: #c92f3c; }
.legend { display: flex; gap: 1rem; font-size: .8rem; margin-bottom: .5rem; flex-wrap: wrap; }
.legend span { display: flex; align-items: center; gap: .35rem; }
.legend span::before { content: ''; display: inline-block; width: 14px; height: 14px;
border-radius: 3px; border: 1px solid #aaa; }
.leg-band::before { background: #b8d4f0; border-color: #5a9ad4; }
.leg-skip::before { background: #f0f0f0; border-color: #ccc; }
.leg-match::before { background: #d4f0c0; border-color: #5ab44a; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-family: ui-monospace, monospace; font-size: .78rem; }
th, td { width: 28px; height: 26px; text-align: center; vertical-align: middle;
border: 1px solid #ddd; padding: 0; }
th { background: #e8eef3; font-weight: 600; color: #444; font-size: .75rem; }
td.band { background: #b8d4f0; color: #1a3c6e; }
td.skip { background: #f0f0f0; color: #bbb; font-style: italic; }
td.match { background: #d4f0c0; color: #1a4a10; font-weight: 700; }
td.outside { background: #f8f8f8; color: #ddd; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ukkonen banded edit-distance demo
// Only cells within k columns of the main diagonal are computed.
const elA = document.getElementById('strA');
const elB = document.getElementById('strB');
const elK = document.getElementById('kVal');
const elKDisp = document.getElementById('kDisplay');
const elResult = document.getElementById('result');
const elWrap = document.getElementById('tableWrap');
// Full O(nm) edit distance — used only to show what would be computed naively.
function editDistanceFull(a, b) {
const n = a.length, m = b.length;
const dp = Array.from({ length: n + 1 }, (_, i) => Array.from({ length: m + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
for (let i = 1; i <= n; i++)
for (let j = 1; j <= m; j++)
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
return { dp, dist: dp[n][m] };
}
// Ukkonen banded edit distance — O(nk) time, O(k) space.
// Fills only cells where |j - i| <= k.
function editDistanceBanded(a, b, k) {
const n = a.length, m = b.length;
const INF = k + 1;
// Initialize boundary cells within the band.
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(INF));
const inBand = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(false));
for (let i = 0; i <= Math.min(n, k); i++) { dp[i][0] = i; inBand[i][0] = true; }
for (let j = 0; j <= Math.min(m, k); j++) { dp[0][j] = j; inBand[0][j] = true; }
for (let i = 1; i <= n; i++) {
// Column window for row i: [max(1, i-k) .. min(m, i+k)]
const jLo = Math.max(1, i - k);
const jHi = Math.min(m, i + k);
for (let j = jLo; j <= jHi; j++) {
inBand[i][j] = true;
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
const del = inBand[i - 1][j] ? dp[i - 1][j] : INF;
const ins = inBand[i][j - 1] ? dp[i][j - 1] : INF;
const sub = inBand[i - 1][j - 1] ? dp[i - 1][j - 1] : INF;
dp[i][j] = 1 + Math.min(del, ins, sub);
}
}
}
return { dp, inBand, dist: inBand[n][m] ? dp[n][m] : INF };
}
function buildTable(a, b, k) {
const n = a.length, m = b.length;
if (n === 0 || m === 0) return;
const { dp: fullDp } = editDistanceFull(a, b);
const { dp: bandDp, inBand, dist } = editDistanceBanded(a, b, k);
// Show edit distance or out-of-band notice.
if (dist <= k) {
elResult.textContent = i18nFmt('Edit distance = %dist% (within band)', { dist: dist });
elResult.className = 'result exact';
} else {
elResult.textContent = i18nFmt('Edit distance > %k% — strings too different for this band', { k: k });
elResult.className = 'result exceeds';
}
// Build the visual DP table.
const table = document.createElement('table');
// Column headers — epsilon then each character of B.
const hdr = table.insertRow();
hdr.insertCell().textContent = '';
hdr.insertCell().textContent = 'ε'; // epsilon
for (const ch of b) { const th = document.createElement('th'); th.textContent = ch; hdr.appendChild(th); }
for (let i = 0; i <= n; i++) {
const row = table.insertRow();
const rh = document.createElement('th');
rh.textContent = i === 0 ? 'ε' : a[i - 1];
row.appendChild(rh);
for (let j = 0; j <= m; j++) {
const td = row.insertCell();
if (!inBand[i][j]) {
td.className = 'skip';
td.textContent = '·'; // middle dot for skipped
} else if (bandDp[i][j] === fullDp[i][j] && bandDp[i][j] === dist && i === n && j === m) {
td.className = 'match';
td.textContent = bandDp[i][j];
} else {
td.className = 'band';
td.textContent = bandDp[i][j] <= k ? bandDp[i][j] : '·';
}
}
}
elWrap.innerHTML = '';
elWrap.appendChild(table);
}
function update() {
const a = elA.value.slice(0, 14);
const b = elB.value.slice(0, 14);
const k = parseInt(elK.value, 10);
elKDisp.textContent = k;
elK.max = Math.max(a.length, b.length, 1);
buildTable(a, b, k);
}
elA.addEventListener('input', update);
elB.addEventListener('input', update);
elK.addEventListener('input', update);
update();
<!-- Banded edit-distance demo — DP table with diagonal band highlighted -->
<div class="controls">
<div class="field">
<label for="strA">String A</label>
<input id="strA" type="text" value="kitten" maxlength="14" spellcheck="false" placeholder="first string" />
</div>
<div class="field">
<label for="strB">String B</label>
<input id="strB" type="text" value="sitting" maxlength="14" spellcheck="false" placeholder="second string" />
</div>
<div class="field field-k">
<label for="kVal">Threshold k = <span id="kDisplay">3</span></label>
<input id="kVal" type="range" min="0" max="14" value="3" />
</div>
</div>
<div id="result" class="result"></div>
<div id="legend" class="legend">
<span class="leg-band">In band (computed)</span>
<span class="leg-skip">Outside band (skipped)</span>
<span class="leg-match">Final answer</span>
</div>
<div id="tableWrap" class="table-wrap"></div>
/* Styles for the banded edit-distance demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .7rem; }
.field { display: flex; flex-direction: column; gap: .2rem; }
.field label { font-size: .8rem; font-weight: 600; color: #555; }
.field input[type=text] { width: 9rem; padding: .3rem .5rem; border: 1px solid #bbb;
border-radius: 6px; font-size: .95rem; font-family: ui-monospace, monospace; }
.field-k label { display: flex; align-items: center; gap: .3rem; }
.field-k input { width: 9rem; cursor: pointer; }
.result { font-weight: 700; font-size: 1rem; min-height: 1.4em; margin-bottom: .4rem; }
.result.exact { color: #0a7d33; }
.result.exceeds { color: #c92f3c; }
.legend { display: flex; gap: 1rem; font-size: .8rem; margin-bottom: .5rem; flex-wrap: wrap; }
.legend span { display: flex; align-items: center; gap: .35rem; }
.legend span::before { content: ''; display: inline-block; width: 14px; height: 14px;
border-radius: 3px; border: 1px solid #aaa; }
.leg-band::before { background: #b8d4f0; border-color: #5a9ad4; }
.leg-skip::before { background: #f0f0f0; border-color: #ccc; }
.leg-match::before { background: #d4f0c0; border-color: #5ab44a; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-family: ui-monospace, monospace; font-size: .78rem; }
th, td { width: 28px; height: 26px; text-align: center; vertical-align: middle;
border: 1px solid #ddd; padding: 0; }
th { background: #e8eef3; font-weight: 600; color: #444; font-size: .75rem; }
td.band { background: #b8d4f0; color: #1a3c6e; }
td.skip { background: #f0f0f0; color: #bbb; font-style: italic; }
td.match { background: #d4f0c0; color: #1a4a10; font-weight: 700; }
td.outside { background: #f8f8f8; color: #ddd; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ukkonen banded edit-distance demo
// Only cells within k columns of the main diagonal are computed.
const elA = document.getElementById('strA');
const elB = document.getElementById('strB');
const elK = document.getElementById('kVal');
const elKDisp = document.getElementById('kDisplay');
const elResult = document.getElementById('result');
const elWrap = document.getElementById('tableWrap');
// Full O(nm) edit distance — used only to show what would be computed naively.
function editDistanceFull(a, b) {
const n = a.length, m = b.length;
const dp = Array.from({ length: n + 1 }, (_, i) => Array.from({ length: m + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
for (let i = 1; i <= n; i++)
for (let j = 1; j <= m; j++)
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
return { dp, dist: dp[n][m] };
}
// Ukkonen banded edit distance — O(nk) time, O(k) space.
// Fills only cells where |j - i| <= k.
function editDistanceBanded(a, b, k) {
const n = a.length, m = b.length;
const INF = k + 1;
// Initialize boundary cells within the band.
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(INF));
const inBand = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(false));
for (let i = 0; i <= Math.min(n, k); i++) { dp[i][0] = i; inBand[i][0] = true; }
for (let j = 0; j <= Math.min(m, k); j++) { dp[0][j] = j; inBand[0][j] = true; }
for (let i = 1; i <= n; i++) {
// Column window for row i: [max(1, i-k) .. min(m, i+k)]
const jLo = Math.max(1, i - k);
const jHi = Math.min(m, i + k);
for (let j = jLo; j <= jHi; j++) {
inBand[i][j] = true;
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
const del = inBand[i - 1][j] ? dp[i - 1][j] : INF;
const ins = inBand[i][j - 1] ? dp[i][j - 1] : INF;
const sub = inBand[i - 1][j - 1] ? dp[i - 1][j - 1] : INF;
dp[i][j] = 1 + Math.min(del, ins, sub);
}
}
}
return { dp, inBand, dist: inBand[n][m] ? dp[n][m] : INF };
}
function buildTable(a, b, k) {
const n = a.length, m = b.length;
if (n === 0 || m === 0) return;
const { dp: fullDp } = editDistanceFull(a, b);
const { dp: bandDp, inBand, dist } = editDistanceBanded(a, b, k);
// Show edit distance or out-of-band notice.
if (dist <= k) {
elResult.textContent = i18nFmt('Edit distance = %dist% (within band)', { dist: dist });
elResult.className = 'result exact';
} else {
elResult.textContent = i18nFmt('Edit distance > %k% — strings too different for this band', { k: k });
elResult.className = 'result exceeds';
}
// Build the visual DP table.
const table = document.createElement('table');
// Column headers — epsilon then each character of B.
const hdr = table.insertRow();
hdr.insertCell().textContent = '';
hdr.insertCell().textContent = 'ε'; // epsilon
for (const ch of b) { const th = document.createElement('th'); th.textContent = ch; hdr.appendChild(th); }
for (let i = 0; i <= n; i++) {
const row = table.insertRow();
const rh = document.createElement('th');
rh.textContent = i === 0 ? 'ε' : a[i - 1];
row.appendChild(rh);
for (let j = 0; j <= m; j++) {
const td = row.insertCell();
if (!inBand[i][j]) {
td.className = 'skip';
td.textContent = '·'; // middle dot for skipped
} else if (bandDp[i][j] === fullDp[i][j] && bandDp[i][j] === dist && i === n && j === m) {
td.className = 'match';
td.textContent = bandDp[i][j];
} else {
td.className = 'band';
td.textContent = bandDp[i][j] <= k ? bandDp[i][j] : '·';
}
}
}
elWrap.innerHTML = '';
elWrap.appendChild(table);
}
function update() {
const a = elA.value.slice(0, 14);
const b = elB.value.slice(0, 14);
const k = parseInt(elK.value, 10);
elKDisp.textContent = k;
elK.max = Math.max(a.length, b.length, 1);
buildTable(a, b, k);
}
elA.addEventListener('input', update);
elB.addEventListener('input', update);
elK.addEventListener('input', update);
update();
Notice that when k is small relative to the string length, the band is a thin strip and most of the table stays empty. Increase k and the band widens — at k≥max(n,m) it covers the full table and the algorithm degenerates to the classic O(nm) approach.
Comments
Loading comments...