Type two strings below and press Compute LCS. The table fills cell by cell — each entry records the length of the longest common subsequence of the prefixes seen so far. When the table is full, the algorithm traces a path back through it to read off the actual shared characters.
<div class="controls">
<label>String A <input id="strA" type="text" value="ABCBDAB" maxlength="12" /></label>
<label>String B <input id="strB" type="text" value="BDCABA" maxlength="12" /></label>
<div class="btns">
<button id="compute" type="button">Compute LCS</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="result" class="result"></div>
<div id="table-wrap" class="table-wrap"></div>
* { 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; align-items: flex-end; margin-bottom: .8rem; }
label { display: flex; flex-direction: column; gap: .2rem; font-size: .85rem; font-weight: 600; color: #444; }
input { font: 700 15px ui-monospace, monospace; padding: .35rem .5rem; border: 1px solid #bcc0c6;
border-radius: 6px; width: 160px; letter-spacing: .08em; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.6em; margin-bottom: .7rem; color: #0a7d33; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-size: .82rem; }
th, td { width: 32px; height: 32px; text-align: center; border: 1px solid #dde2e8; }
th { background: #edf1f5; font-weight: 700; font-family: ui-monospace, monospace; color: #1d3557; }
td { font-family: ui-monospace, monospace; }
td.match { background: #d4edda; color: #0a5523; font-weight: 700; }
td.path { background: #cde4f5; }
td.path.match { background: #a8d8a0; }
td.arrow { font-size: .7rem; color: #888; vertical-align: bottom; line-height: 1; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Longest Common Subsequence via bottom-up dynamic programming.
// dp[i][j] = length of LCS of A[0..i-1] and B[0..j-1].
// Recurrence:
// if A[i-1] === B[j-1]: dp[i][j] = dp[i-1][j-1] + 1
// else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
// Traceback reads the LCS by following the arrows back from dp[m][n].
function computeLCS(A, B) {
const m = A.length, n = B.length;
// Allocate (m+1) x (n+1) table, all zeros.
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
// dir[i][j]: 'D'=diagonal (match), 'U'=up, 'L'=left
const dir = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(''));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (A[i - 1] === B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
dir[i][j] = 'D'; // diagonal
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
dir[i][j] = 'U'; // up
} else {
dp[i][j] = dp[i][j - 1];
dir[i][j] = 'L'; // left
}
}
}
// Traceback to recover the LCS string and path cells.
const path = new Set();
let i = m, j = n;
let lcs = '';
while (i > 0 && j > 0) {
path.add(i + ',' + j);
if (dir[i][j] === 'D') { lcs = A[i - 1] + lcs; i--; j--; }
else if (dir[i][j] === 'U') { i--; }
else { j--; }
}
return { dp, dir, path, lcs };
}
const ARROW = { D: '↖', U: '↑', L: '←', '': '' };
function render(A, B) {
const { dp, dir, path, lcs } = computeLCS(A, B);
const m = A.length, n = B.length;
document.getElementById('result').textContent =
lcs.length > 0
? fmt('LCS (length %length%): %lcs%', { length: lcs.length, lcs: lcs })
: 'No common subsequence found.';
// Build header row: empty corner, then B characters.
let html = '<table><thead><tr><th></th><th>ε</th>';
for (let j = 0; j < n; j++) html += '<th>' + B[j] + '</th>';
html += '</tr></thead><tbody>';
for (let i = 0; i <= m; i++) {
html += '<tr>';
// Row header: ε for row 0, else A[i-1]
html += '<th>' + (i === 0 ? 'ε' : A[i - 1]) + '</th>';
for (let j = 0; j <= n; j++) {
const isPath = path.has(i + ',' + j);
const isMatch = dir[i][j] === 'D';
let cls = isPath ? (isMatch ? 'path match' : 'path') : (isMatch ? 'match' : '');
const arrow = ARROW[dir[i][j]] || '';
// Show value; append tiny arrow in a span
html += '<td class="' + cls + '">'
+ dp[i][j]
+ (arrow ? '<span class="arrow">' + arrow + '</span>' : '')
+ '</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('table-wrap').innerHTML = html;
}
document.getElementById('compute').addEventListener('click', function () {
const A = document.getElementById('strA').value.toUpperCase().replace(/[^A-Z]/g, '');
const B = document.getElementById('strB').value.toUpperCase().replace(/[^A-Z]/g, '');
if (!A || !B) { document.getElementById('result').textContent = 'Enter two non-empty strings.'; return; }
render(A, B);
});
document.getElementById('reset').addEventListener('click', function () {
document.getElementById('strA').value = 'ABCBDAB';
document.getElementById('strB').value = 'BDCABA';
document.getElementById('result').textContent = '';
document.getElementById('table-wrap').innerHTML = '';
});
// Auto-run on load with the default strings.
render('ABCBDAB', 'BDCABA');
<div class="controls">
<label>String A <input id="strA" type="text" value="ABCBDAB" maxlength="12" /></label>
<label>String B <input id="strB" type="text" value="BDCABA" maxlength="12" /></label>
<div class="btns">
<button id="compute" type="button">Compute LCS</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="result" class="result"></div>
<div id="table-wrap" class="table-wrap"></div>
* { 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; align-items: flex-end; margin-bottom: .8rem; }
label { display: flex; flex-direction: column; gap: .2rem; font-size: .85rem; font-weight: 600; color: #444; }
input { font: 700 15px ui-monospace, monospace; padding: .35rem .5rem; border: 1px solid #bcc0c6;
border-radius: 6px; width: 160px; letter-spacing: .08em; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.6em; margin-bottom: .7rem; color: #0a7d33; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; font-size: .82rem; }
th, td { width: 32px; height: 32px; text-align: center; border: 1px solid #dde2e8; }
th { background: #edf1f5; font-weight: 700; font-family: ui-monospace, monospace; color: #1d3557; }
td { font-family: ui-monospace, monospace; }
td.match { background: #d4edda; color: #0a5523; font-weight: 700; }
td.path { background: #cde4f5; }
td.path.match { background: #a8d8a0; }
td.arrow { font-size: .7rem; color: #888; vertical-align: bottom; line-height: 1; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Longest Common Subsequence via bottom-up dynamic programming.
// dp[i][j] = length of LCS of A[0..i-1] and B[0..j-1].
// Recurrence:
// if A[i-1] === B[j-1]: dp[i][j] = dp[i-1][j-1] + 1
// else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
// Traceback reads the LCS by following the arrows back from dp[m][n].
function computeLCS(A, B) {
const m = A.length, n = B.length;
// Allocate (m+1) x (n+1) table, all zeros.
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
// dir[i][j]: 'D'=diagonal (match), 'U'=up, 'L'=left
const dir = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(''));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (A[i - 1] === B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
dir[i][j] = 'D'; // diagonal
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
dir[i][j] = 'U'; // up
} else {
dp[i][j] = dp[i][j - 1];
dir[i][j] = 'L'; // left
}
}
}
// Traceback to recover the LCS string and path cells.
const path = new Set();
let i = m, j = n;
let lcs = '';
while (i > 0 && j > 0) {
path.add(i + ',' + j);
if (dir[i][j] === 'D') { lcs = A[i - 1] + lcs; i--; j--; }
else if (dir[i][j] === 'U') { i--; }
else { j--; }
}
return { dp, dir, path, lcs };
}
const ARROW = { D: '↖', U: '↑', L: '←', '': '' };
function render(A, B) {
const { dp, dir, path, lcs } = computeLCS(A, B);
const m = A.length, n = B.length;
document.getElementById('result').textContent =
lcs.length > 0
? fmt('LCS (length %length%): %lcs%', { length: lcs.length, lcs: lcs })
: 'No common subsequence found.';
// Build header row: empty corner, then B characters.
let html = '<table><thead><tr><th></th><th>ε</th>';
for (let j = 0; j < n; j++) html += '<th>' + B[j] + '</th>';
html += '</tr></thead><tbody>';
for (let i = 0; i <= m; i++) {
html += '<tr>';
// Row header: ε for row 0, else A[i-1]
html += '<th>' + (i === 0 ? 'ε' : A[i - 1]) + '</th>';
for (let j = 0; j <= n; j++) {
const isPath = path.has(i + ',' + j);
const isMatch = dir[i][j] === 'D';
let cls = isPath ? (isMatch ? 'path match' : 'path') : (isMatch ? 'match' : '');
const arrow = ARROW[dir[i][j]] || '';
// Show value; append tiny arrow in a span
html += '<td class="' + cls + '">'
+ dp[i][j]
+ (arrow ? '<span class="arrow">' + arrow + '</span>' : '')
+ '</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('table-wrap').innerHTML = html;
}
document.getElementById('compute').addEventListener('click', function () {
const A = document.getElementById('strA').value.toUpperCase().replace(/[^A-Z]/g, '');
const B = document.getElementById('strB').value.toUpperCase().replace(/[^A-Z]/g, '');
if (!A || !B) { document.getElementById('result').textContent = 'Enter two non-empty strings.'; return; }
render(A, B);
});
document.getElementById('reset').addEventListener('click', function () {
document.getElementById('strA').value = 'ABCBDAB';
document.getElementById('strB').value = 'BDCABA';
document.getElementById('result').textContent = '';
document.getElementById('table-wrap').innerHTML = '';
});
// Auto-run on load with the default strings.
render('ABCBDAB', 'BDCABA');
Notice how the diagonal arrows mark the matches: every time both strings share a character in the same relative position, the LCS length ticks up by one. Horizontal and vertical arrows mean "skip a character" from one string or the other. The final answer sits in the bottom-right corner of the table.
Comments
Loading comments...