Easy Problemsstringoptimization

Longest Common Substring

Given two strings, find the length of their longest common substring: a run of characters that must appear contiguously, in the same order, within both strings.

Do this lesson first: longest common subsequence

Example input

A = "XABCDEZ", B = "BCYXABCDAZ"

Expected output

5

Break it down

Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.

Fill the table

The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.

Step not started

Press start. The animation stops at every cell YOUR recurrence must fill.

Row 0 and column 0 are given as 0, because no run can end on a pair where one of the two prefixes is empty: that side has no letter to pair with. At the drive size that is 18 bases: 11 across row 0 for columns 0 to 10, and 7 more down column 0 for rows 1 to 7. A computed cell reads at most one cell, the diagonal up and to the left, and a row-major sweep, left to right within each row, has always finished it, since the sweep completed the whole previous row first. A mismatch reads nothing at all. THE RESET IS WHY THE ANSWER IS A SCAN. On the LCS page no cell is ever smaller than the cell above it or the cell to its left, so the last cell holds the answer. Here a mismatch drops a cell to 0 however long the run beside it, so the last cell reports only on the run that ends at both strings' final letters: dp[7][10] = 1, the shared Z, against the answer 5 at dp[5][8]. At lengths 6 to 9 the last cell holds 0. The answer is the largest cell anywhere in the table, so a running best has to travel with the sweep.

Rows ↓
Cols →
∅
1 B
2 C
3 Y
4 X
5 A
6 B
7 C
8 D
9 A
10 Z
∅
0
0
0
0
0
0
0
0
0
0
0
1 X
0
0
0
0
1
0
0
0
0
0
0
2 A
0
0
0
0
0
2
0
0
0
1
0
3 B
0
1
0
0
0
0
3
0
0
0
0
4 C
0
0
2
0
0
0
0
4
0
0
0
5 D
0
0
0
0
0
0
0
0
5
0
0
6 E
0
0
0
0
0
0
0
0
0
0
0
7 Z
0
0
0
0
0
0
0
0
0
0
1

Rows ↓A: XABCDEZ

Cols →B: the first n letters of BCYXABCDAZ

longest-common-substring.ts
  1. 1function longestCommonSubstring(a, b) {
  2. 2 const m = a.length, n = b.length;
  3. 3 const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
  4. 4 let best = 0;
  5. 5 for (let i = 1; i <= m; i++) {
  6. 6 for (let j = 1; j <= n; j++) {
  7. 7 dp[i][j] = a[i - 1] === b[j - 1]
  8. 8 ? dp[i - 1][j - 1] + 1
  9. 9 : 0;
  10. 10 best = Math.max(best, dp[i][j]);
  11. 11 }
  12. 12 }
  13. 13 return best;
  14. 14}
Base caseComputedBeing readAnswer

The code, the trap, the variations

longest-common-substring.ts
  1. 1function longestCommonSubstring(a, b) {
  2. 2 const m = a.length, n = b.length;
  3. 3 const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
  4. 4 let best = 0;
  5. 5 for (let i = 1; i <= m; i++) {
  6. 6 for (let j = 1; j <= n; j++) {
  7. 7 dp[i][j] = a[i - 1] === b[j - 1]
  8. 8 ? dp[i - 1][j - 1] + 1
  9. 9 : 0;
  10. 10 best = Math.max(best, dp[i][j]);
  11. 11 }
  12. 12 }
  13. 13 return best;
  14. 14}

Where people go wrong

Carrying LCS's mismatch rule over: writing max(dp[i - 1][j], dp[i][j - 1]) where this page writes 0. The table that rule builds is the LCS table, and on this pair it hides well. Read as its last cell or as its largest cell, which is the same number in that table, it returns 3, 4, 5, 5 at lengths 6 to 9, right at every one, because up to length 9 the longest common subsequence of these strings happens to be contiguous. At length 10 it returns 6, for XABCDZ, which is not a substring of either string: in A an E sits between the D and the Z, and in B an A does. One slider position in five catches it, so test on an input where the subsequence and the substring differ. The second trap is LCS's other habit, returning dp[m][n]; here that gives 0, 0, 0, 0, 1 across the five lengths, wrong at every one.

  • Return the longest common substring itself, not just its length.

    The table, its recurrence and its bases do not change; the running best also records WHERE it was found, the cell (i, j). A run is contiguous, so its end and its length pin it completely: the answer is A's letters i - best + 1 to i, which are also B's letters j - best + 1 to j. Here that is the cell (5, 8) with length 5, A's letters 1 to 5, XABCD. No walk back through the table is needed, unlike LCS, where a subsequence's letters are scattered and have to be recovered one step at a time, so the O(1) diagonal sweep from the execution panel still works. A tie needs a rule. With B's first 5 letters, below this slider's range, dp[2][5] (XA) and dp[4][2] (BC) both reach 2; keeping the first maximum in row-major order prints XA, and keeping the last prints BC.

  • Allow up to k mismatched positions: the longest pair of equal-length substrings, one from each string, that differ in at most k places.

    The state gains a third index, t from 0 to k, the most mismatches the run may contain: dp[i][j][t] is the longest such run ending at A's i-th and B's j-th letter with at most t mismatches. A match still extends the diagonal, dp[i - 1][j - 1][t] + 1. A mismatch no longer forces 0: with t > 0 it spends one and extends dp[i - 1][j - 1][t - 1] + 1, and only at t = 0 is it the reset this page performs. The bases do not change: row 0 and column 0 are 0 at every t, since no run can end on a pair with an empty prefix however many mismatches it may spend. The t = 0 layer is exactly this page's table, the sweep stays row-major, and the cost grows to O(m × n × (k + 1)). On this pair, k = 1 lifts the answer from 5 to 7: all of A, XABCDEZ, against B's last seven letters, XABCDAZ, which differ only where A has its E and B has an A.