HARD Lab · Dynamic Programming · 31

1143. Longest Common Subsequence

Find the longest sequence of characters that appears in both strings in the same relative order (not necessarily contiguous). A classic 2D dynamic-programmingtable lets you build the answer in O(m × n) with a single elegant recurrence: match → diagonal + 1, no-match → max of up or left.

Medium2D DPTypeScript

PROBLEM What we're solving

Given two strings, return the length of their longest common subsequence (LCS) — a subsequence that appears in both strings in the same relative order, but not necessarily contiguously. text1="abcde", text2="ace" 3 (the subsequence "ace"appears in both). text1="abc", text2="abc" 3. text1="abc", text2="def" 0.

KEY IDEA Build the answer character-by-character in a table

Insight → define dp[i][j] as the LCS length of text1[0..i-1] and text2[0..j-1]. The recurrence has only two cases:
  • Characters match: dp[i][j] = dp[i-1][j-1] + 1 — the LCS extends diagonally.
  • No match: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) — skip one character from either string, take the better result.
Every subproblem depends only on previously-filled cells, so we fill row-by-row, left-to-right.

RECURRENCE Step-by-step recipe

  • 0 · Allocate table. Create an (m+1)×(n+1) table, all zeros. The extra row and column represent the empty-string base cases: LCS with an empty string is 0.
  • 1 · Iterate i from 1→m, j from 1→n. For each cell, compare text1[i-1] and text2[j-1].
  • 2a · Match. dp[i][j] = dp[i-1][j-1] + 1. The diagonal inherits the best LCS of both shorter prefixes, plus this shared character.
  • 2b · No match. dp[i][j] = max(dp[i-1][j], dp[i][j-1]). We can only skip one character; take the skip that left the longer LCS.
  • 3 · Answer. dp[m][n] is the final result. Backtrace diagonally whenever a match drove the value to recover the actual subsequence.
Classic confusion → people conflate LCS (subsequence — gaps allowed) with Longest Common Substring (contiguous — no gaps). For substring the recurrence resets on mismatch (dp[i][j] = 0 instead of max(up,left)); the answer is the table maximum, not the corner cell.
Pattern transfer → the same 2D DP skeleton underlies Edit Distance (add cost to the no-match case), Shortest Common Supersequence (derive from LCS table), Longest Palindromic Subsequence (LCS of string with its reverse), and Distinct Subsequences.

COST Complexity & space optimisation

Recursive without memo
O(2ⁿ)
Recomputes every overlapping subproblem.
2D DP table
O(m·n)
O(m·n) time; O(m·n) space (or O(n) with rolling row).

Space optimisation

Because each row only looks at the row above, you can keep two 1-D arrays — prev and curr— cutting space from O(m × n) to O(n). A single-array version is also possible with a diagonal save variable. Note: you lose backtrace ability with rolling rows.

RUN IT Fill the DP table, then trace the LCS

step 0 / 37
STARTAllocate a 6×4 table. dp[i][j] = length of the LCS of the first i chars of “abcde” and the first jchars of “ace”. Base cases (row 0 and col 0) are already 0.
ε
a
c
e
ε
0
0
0
0
a
0
0
0
0
b
0
0
0
0
c
0
0
0
0
d
0
0
0
0
e
0
0
0
0
Current comparison
waiting…
LCS result
revealed at end
current cellsource cellschosen sourceLCS path
slowfast

TYPESCRIPT The solution, annotated

longestCommonSubsequence.ts
function longestCommonSubsequence(text1: string, text2: string): number {
  const m = text1.length;
  const n = text2.length;
  // dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]
  const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;   // match: extend diagonal
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // no match: best of up/left
      }
    }
  }
  return dp[m][n];
}

Reading it block by block

Lines 4–5 — allocate. We build an (m+1)×(n+1) table filled with zeros. Row 0 and column 0 model the empty-prefix base cases: LCS of any string with the empty string is 0.
Lines 7–8 — nested iteration. We process every pair of prefixes in row-major order. Because dp[i][j]only reads cells above and to the left, they're already filled when we need them.
Line 9–10 — match branch. When text1[i-1] === text2[j-1] the two characters can extend the LCS of the two shorter prefixes. We inherit the diagonal value and add 1.
Lines 11–12 — no-match branch. We can skip one character from either side. Takingmax(dp[i-1][j], dp[i][j-1]) picks the skip that preserves the longest LCS found so far.
Line 15 — answer. The bottom-right corner dp[m][n]is the LCS length of the full strings. To recover the actual subsequence, backtrace: whenever the match branch drove a cell's value, step diagonally and record that character.
Complexity → O(m × n) time (all cells visited once); O(m × n) space for the full table, reducible to O(n) with a rolling two-row approach.

INTERVIEWFollow-ups they'll ask

  • “Print the actual LCS, not just its length?” Backtrace from dp[m][n]: whenever text1[i-1] === text2[j-1] drove the value, prepend that char and step diagonally; otherwise move toward the larger neighbour.
  • “Reduce space to O(n)?” Keep two rows — prev and curr — swapping each iteration. A single-array solution uses a diag temp variable.
  • “How does this relate to Edit Distance?”Edit Distance adds a cost of 1 to the no-match case and tracks insert/delete/replace; LCS is the “free skip” version.
  • “Longest Palindromic Subsequence?” LCS of text1 with its reverse — same code, different inputs.
  • “Distinct Subsequences?” A counting variant of the same 2D table: instead of max, sum the ways; match adds the diagonal count.

MNEMONIC The one-liner

"Match → diagonal + 1; no-match → max(up, left). The corner is the answer."

TRIGGERS When you see ___ → reach for ___

"longest common subsequence / LCS"2D DP table, m×n
two strings, order matters, gaps alloweddp[i][j] = diagonal or max(up,left)
backtrace for actual subsequencefollow diagonal on matches from dp[m][n]
palindromic subsequence / supersequenceLCS of string with reverse / derived from LCS

SKELETON The reusable shape

skeleton.ts
function longestCommonSubsequence(text1: string, text2: string): number {
  const m = text1.length, n = text2.length;
  const dp = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0));
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[m][n];
}

FLASHCARDS Tap to flip

What does dp[i][j] represent in the LCS table?
The length of the LCS of text1[0..i-1] and text2[0..j-1].
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is dp[i][j] when text1[i-1] === text2[j-1]?
QUESTION 02
For text1="abcde", text2="ace", the LCS length is:
QUESTION 03
Time complexity of the 2D DP approach?
QUESTION 04
What distinguishes LCS (longest common subsequence) from Longest Common Substring?
QUESTION 05
Which cells does dp[i][j] depend on when there is no match?
QUESTION 06
To reduce space from O(m·n) to O(n) you:
QUESTION 07
Which problem is directly solvable by calling LCS on a string and its reverse?