Lesson 52 - DP VI: two strings, one grid

~20 minutes · Day 68 · 2D dynamic programming

Tonight's interview shape is two strings and one question: how much order do they share? Longest Common Subsequence asks for the length of the longest sequence that appears in both strings without changing order. A subsequence may skip characters; it does not have to be contiguous (Wikipedia: longest common subsequence).

The invariant

When characters match, extend the diagonal: dp[i][j] = dp[i-1][j-1] + 1; otherwise take the better of dropping one character from either string.

That's the whole grid. Every cell answers a smaller version of the same question: “what is the LCS length for these two prefixes?”

LCS: the slow way first

One brute force idea is to generate every subsequence of text1, then check whether each one appears as a subsequence of text2. That is O(2m · n) time in the usual two-string case, because one string has exponentially many subsequences and each check can scan the other string. It works as a thought experiment, but it is too slow for interviews.

The repeated work is the clue. The same prefix pairs show up again and again. Dynamic programming keeps one answer per prefix pair, so each state is computed once.

The grid idea

Define dp[i][j] as the LCS length between text1[:i] and text2[:j]. Row 0 and column 0 are zero: an empty string shares nothing with anything. Then fill left-to-right, top-to-bottom.

text2 prefixes text1 prefixes empty a c e empty a b c d e 0 0 0 0 0 1 1 1 0 1 1 1 0 1 2 2 0 1 2 2 0 1 2 3 match: diagonal + 1 a/a, c/c, e/e extend the subsequence mismatch: max(top, left) drop one character, keep the better prefix

Now the code is just the diagram written carefully:

def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]  # empty prefix row/col
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:   # last chars match
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]                             # full text1 vs full text2

The index shift is normal: dp[i][j] talks about prefix lengths, while text1[i - 1] and text2[j - 1] are the last characters inside those prefixes.

Transfer: same grid, new costume

Edit Distance also puts one string on rows and the other on columns. Before revealing: what are the four DP questions here - state, transition, base case, answer location?

Your prediction first - then reveal.

The state is still dp[i][j] for two prefixes. The base row and column mean turning a prefix into an empty string. The answer still lives at dp[m][n]. The transition changes: if characters match, take the diagonal unchanged; otherwise choose the cheapest insert, delete, or replace from neighboring states. Same two-string grid, different cell rule.

What the trade buys you

ApproachTimeSpace
Brute force (try subsequences)O(2m · n)O(m)
2D DP (one cell per prefix pair)O(mn)O(mn)

Here m = len(text1) and n = len(text2). A full table is the clearest first version; later, the same recurrence can be compressed because each row only needs the row above and the current row's left cell.

Practice - answer before you scroll past

1. What does dp[i][j] mean in this lesson?

2. When the two current characters match, what happens?

3. Spot the bug in this loop:

for i in range(1, len(text1) + 1):
    for j in range(1, len(text2) + 1):
        if text1[i] == text2[j]:
            dp[i][j] = dp[i - 1][j - 1] + 1

4. Which task asks for this pattern?

Your move (tonight)

Solve Longest Common Subsequence using the cycle - predict → attempt → compare → encode - in your Notion tracker.

  1. Predict: state dp[i][j], the base row/column, the match rule, and the mismatch rule before coding.
  2. Attempt: write the full O(mn) table version first. Do not optimize space tonight.
  3. Compare: watch the source below only after your attempt, and note where your reasoning diverged.
  4. Encode: write the invariant from this page in your own words.
One primary source

Watch NeetCode - Longest Common Subsequence after your attempt, as the compare step. Look for the moment where the diagonal move becomes obvious.


Stuck or curious? Ask your teacher (the agent) - that's what it's for.