Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Dynamic Programming

Builds on Grid DP, which you haven’t finished yet.

P1MediumBuilt on Grid DP~4 min speedrun

Two-Sequence DP (Longest Common Subsequence, Edit Distance)

Built on
Grid DPThe two axes of the table are the prefixes of two sequences, and a cell also looks at the cell diagonally up and to the left.
Grows into
Distinct Subsequences
The problem

Longest Common Subsequence (LC 1143)(opens leetcode.com)

You're handed two strings, text1 and text2. A subsequence of a string is whatever remains after deleting zero or more of its characters while keeping the survivors in their original order -- for instance, "ace" can be pulled out of "abcde" this way. A common subsequence is a subsequence the two strings share.

Report the length of the longest subsequence they have in common, or 0 if they share none.

Example 1
Input:text1 = "abcde", text2 = "ace"
Output:3
Both strings share the subsequence "ace", giving a length of 3.

Two-Sequence DP

dp[i][j] = the answer for text1[:i] and text2[:j] ➔ built from the cell above, the cell to the left and the diagonal ➔ 1D rolling row
Spot it when: Two strings or arrays are compared or transformed (common subsequence, edit distance, wildcard matching), and the answer for prefixes of both is built from answers for shorter prefixes.
Two-Sequence DP
Python
1def longest_common_subsequence(text1: str, text2: str) -> int:
2 """Longest Common Subsequence with two rolling rows.
3 Time: O(M * N) | Space: O(min(M, N))
4 """
5 if len(text1) < len(text2):
6 text1, text2 = text2, text1 # text2 is the shorter string
7 
8 m, n = len(text1), len(text2)
9 # prev, curr: two rolling rows of the DP table; index 0 of each row is the empty prefix
10 prev = [0] * (n + 1)
11 
12 for i in range(1, m + 1):
13 curr = [0] * (n + 1)
14 for j in range(1, n + 1):
15 # 1. Extend Or Drop One Character:
16 # A match extends the diagonal (prev[j - 1] + 1); otherwise drop one of the two last characters.
17 if text1[i - 1] == text2[j - 1]:
18 curr[j] = prev[j - 1] + 1
19 else:
20 curr[j] = max(prev[j], curr[j - 1])
21 # 2. Roll The Row Forward:
22 # curr becomes the finished row i; it is read as the row above while filling row i + 1.
23 prev = curr
24 
25 return prev[n]
Invariant: After row i is filled, prev[j] is the answer for text1[:i] and text2[:j] for every j.

Let the two axes of the table be the prefixes text1[:i] and text2[:j]. Cell (i, j) depends on three neighbours: the cell above (drop the last character of text1), the cell to the left (drop the last character of text2) and the diagonal cell (use both last characters when they match). Rows are filled in order, so all three are final, and two rolling rows are enough. The trap is the index: row i ends with text1[i - 1], so compare text1[i - 1] == text2[j - 1]; comparing text1[i] reads one character too far.

See it run

Longest Common Subsequence, step by step

i - · j - · curr[j] -

i - · j - · curr[j] -

i 1 · j 1 · curr[j] 1

i 1 · j 2 · curr[j] 1

i 1 · j 3 · curr[j] 1

i 2 · j 1 · curr[j] 1

i 2 · j 2 · curr[j] 1

i 2 · j 3 · curr[j] 1

i 3 · j 1 · curr[j] 1

i 3 · j 2 · curr[j] 2

i 3 · j 3 · curr[j] 2

i 4 · j 1 · curr[j] 1

i 4 · j 2 · curr[j] 2

i 4 · j 3 · curr[j] 2

i 5 · j 1 · curr[j] 1

i 5 · j 2 · curr[j] 2

i 5 · j 3 · curr[j] 3

prev[n] 3

Row i is the prefix text1[:i]. Column j is the prefix text2[:j]. A cell is the longest common subsequence length of those two prefixes.

  • new the cell being written
  • diag the cell one row up and one column left, read on a match
  • above the cell one row up, read on a mismatch
  • left the cell one column left, read on a mismatch
  • answer the final answer

Start: text1 = "abcde" and text2 = "ace". Row i is the prefix text1[:i]. Column j is the prefix text2[:j].

prev = [0] * (n + 1) is row 0, and curr[0] = 0 starts every row. An empty prefix shares no letters with any prefix, so these cells are 0.

i = 1, j = 1: text1[0] = "a" equals text2[0] = "a", so curr[1] = prev[0] + 1 = 0 + 1 = 1.

i = 1, j = 2: text1[0] = "a" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(0, 1) = 1.

i = 1, j = 3: text1[0] = "a" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(0, 1) = 1.

i = 2, j = 1: text1[1] = "b" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 2, j = 2: text1[1] = "b" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(1, 1) = 1.

i = 2, j = 3: text1[1] = "b" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(1, 1) = 1.

i = 3, j = 1: text1[2] = "c" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 3, j = 2: text1[2] = "c" equals text2[1] = "c", so curr[2] = prev[1] + 1 = 1 + 1 = 2.

i = 3, j = 3: text1[2] = "c" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(1, 2) = 2.

i = 4, j = 1: text1[3] = "d" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 4, j = 2: text1[3] = "d" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(2, 1) = 2.

i = 4, j = 3: text1[3] = "d" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(2, 2) = 2.

i = 5, j = 1: text1[4] = "e" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 5, j = 2: text1[4] = "e" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(2, 1) = 2.

i = 5, j = 3: text1[4] = "e" equals text2[2] = "e", so curr[3] = prev[2] + 1 = 2 + 1 = 3.

The loops end. prev[3] = 3, the length of the longest common subsequence of "abcde" and "ace".

empty
empty
a
ac
ace
a
ab
abc
abcd
abcde
1 / 18
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Comparing text1[i] and text2[j] without the - 1

    Mistake
    if text1[i] == text2[j]:
    Fix
    if text1[i - 1] == text2[j - 1]:

    What happens: The table has an extra row and column for the empty prefix. So row i stands for the prefix text1[:i], whose last character is text1[i - 1]. In the same way, column j ends with text2[j - 1]. text1[i] and text2[j] are one character too far. At j = n, text2[j] reads past the end of text2.

    Example: text1 = "abc", text2 = "ab"
    Mistake: raises IndexErrorFix: returns 2
  2. 2

    Gotcha 2: Reading the diagonal from curr[j - 1] instead of prev[j - 1]

    Mistake
    curr[j] = curr[j - 1] + 1
    Fix
    curr[j] = prev[j - 1] + 1

    What happens: A match extends the diagonal cell dp[i-1][j-1]. With two rolling rows, that cell is prev[j - 1]. curr[j - 1] is the cell to the left in the current row, so one character of text1 can match twice. With a single 1D row, save the diagonal in a temporary variable before you overwrite it.

    Example: text1 = "ab", text2 = "bb"
    Mistake: returns 2Fix: returns 1
  3. 3

    Gotcha 3: Using 0 for every empty-prefix base cell

    Mistake
    dp[i][0] = 0
    dp[0][j] = 0
    Fix
    dp[i][0] = i
    dp[0][j] = j

    What happens: The empty-prefix row and column hold base cases, and their values depend on the problem. For the LCS, they are 0. In Edit Distance (LC 72), turning word1[:i] into the empty string takes i deletions. So dp[i][0] = i and dp[0][j] = j.

    Example: Edit Distance (LC 72): word1 = "horse", word2 = "ros"
    Mistake: returns 2Fix: returns 3
Not this one if…
  • You walk right and down through one grid and no second sequence is compared, as in Unique Paths (LC 62): each cell needs only the cell above and the cell to the left. See Grid DP
  • You must count how many different ways one string can be picked out of the other, as in Distinct Subsequences (LC 115), not the length or cost of one best alignment. See Distinct Subsequences

Deep Dive

What a cell means when the axes are two strings

Grid DP already taught the table and the row-by-row fill. This card changes what the two axes are. Row i stands for the prefix text1[:i], and column j stands for the prefix text2[:j]. A prefix is the first characters of a string.

The cell (i, j) is the answer for that pair of prefixes. In longest_common_subsequence, it is the length of the longest common subsequence of text1[:i] and text2[:j]. A subsequence keeps the order of characters but may skip characters.

Index 0 on each axis is the empty prefix. These extra cells let the first real character use the same formula as every other character. The number of states is (m + 1) * (n + 1).

Why the recurrence is correct

Look at the last characters text1[i - 1] and text2[j - 1]. If they are equal, some best common subsequence can end with that pair. Taking the pair never loses length, so the answer is prev[j - 1] + 1. The cell on the diagonal is the answer for the two prefixes without those last characters.

If they differ, they cannot both be the last matched pair. At least one of them is not used. So the answer is the better of dropping the last character of text1, which is prev[j], or dropping the last character of text2, which is curr[j - 1].

Example: for text1 = "abcde" and text2 = "ace", the final row is [0, 1, 2, 3], so the answer is 3, the subsequence "ace". If the match case is removed and every cell uses max(prev[j], curr[j - 1]), all cells stay 0, and "abc" against "abc" returns 0 instead of 3.

Why two rows are needed, and what is lost

A match reads the diagonal cell prev[j - 1]. In a single row, row[j - 1] is overwritten by the current row before it is needed as the diagonal. So the code keeps two rows, prev and curr. After the inner loop, prev = curr makes the finished row the row above for the next i.

The code swaps the strings so that text2 is the shorter one. The length of the longest common subsequence does not change when the strings swap places. So each row has min(m, n) + 1 entries.

Rolling rows keep only the last row. You can read the length, but you cannot walk back through the table to print a subsequence. For that, keep the full table.

The cost, and the same shape for Edit Distance

Each cell reads at most three cells and does one comparison. The time is O(M * N). For two strings of length 1000, that is about one million cell updates. The space is O(min(M, N)) for the two rows.

Edit Distance (LC 72) uses the same table. The base cells are not 0. Turning word1[:i] into the empty string takes i deletions, so dp[i][0] = i and dp[0][j] = j. On a match the cell copies the diagonal. On a mismatch it is 1 + min(above, left, diagonal), which stands for a delete, an insert, or a replace. For "horse" and "ros" the answer is 3.

So the work to learn is the meaning of the three neighbours and the base cells. The loops stay the same.

60-Second Pitch

"A brute force tries every subsequence of one string and checks it against the other, which takes exponential time. I define one cell (i, j) as the answer for the first i characters of one string and the first j characters of the other. Each cell depends on the cell above, the cell to the left, and the diagonal cell. If the last characters match, I take the diagonal value plus one. If they differ, I take the larger of the cell above and the cell to the left. There are M times N cells with constant work each, so the time is O(M * N). I keep only two rows of the shorter string's length, so the space is O(min(M, N)). If the problem needs the actual subsequence, I keep the full table and walk back from the last cell."

Common Questions

How do you print the actual longest common subsequence, not only its length?

Keep the full table, with M + 1 rows and N + 1 columns. Start at the last cell. If the last characters match, add that character to the result and move to the diagonal cell. If not, move to the neighbour with the larger value, the cell above or the cell to the left. Reverse the collected characters at the end. This uses O(M * N) space.

What are the time and space complexity of longest_common_subsequence?

O(M * N) time, because each of the (M + 1) * (N + 1) cells does constant work. The space is O(min(M, N)), because the code keeps two rows of the shorter string's length plus one.

Why does the code need two rows when Unique Paths (LC 62) needs one?

A match reads the diagonal cell prev[j - 1]. In one row, that cell has already been overwritten by the current row. Unique Paths reads only the cell above and the cell to the left, so one row is enough there.

How does Edit Distance (LC 72) differ from this table?

The grid and the three neighbours are the same. The base cells are i and j, because turning a prefix into the empty string costs one deletion per character. On a match the cell copies the diagonal. On a mismatch it is 1 + min(above, left, diagonal). It takes a minimum instead of a maximum.

How does Delete Operation for Two Strings (LC 583) use the same table?

Only the characters in a common subsequence can stay, and every other character is deleted. So the answer is len(text1) + len(text2) - 2 * lcs, where lcs is the result of longest_common_subsequence. For "sea" and "eat" the longest common subsequence has length 2, so the answer is 2.

SWE Reasoning

Spot it

  • Two strings or two arrays are compared, aligned, or turned into each other: common subsequence, edit distance, wildcard or pattern matching.
  • The answer for a pair of prefixes (i, j) can be built from the answers for (i - 1, j), (i, j - 1) and (i - 1, j - 1).
  • The sizes are about 1000 or less on each side, so M * N cells is fine, and trying every subsequence is exponential.
  • The problem asks for one number (a length, a count, a minimum cost), not for every solution listed.
  • Counting the number of different ways, as in Distinct Subsequences (LC 115), also fits this table, with a sum in place of a maximum.

The invariant, step by step

  1. Before the loop, prev is all zeros. It describes the empty prefix of text1, which has no common subsequence with any prefix of text2.
  2. At row i, curr[0] is 0, because an empty prefix of text2 matches nothing.
  3. For each j, the code compares text1[i - 1] with text2[j - 1]. These are the last characters of the two prefixes.
  4. On a match, curr[j] = prev[j - 1] + 1. The pair extends the best answer for the two shorter prefixes.
  5. On a mismatch, curr[j] = max(prev[j], curr[j - 1]). One of the two last characters is dropped.
  6. After the inner loop, prev = curr. Now prev[j] is the answer for text1[:i] and text2[:j] for every j, which is the card's invariant. The last step returns prev[n].

Traps

  • Compare text1[i] with text2[j]. The last character of prefix i is text1[i - 1]. Without the - 1, text2[j] reads past the end at j = n and raises IndexError.
  • Read the diagonal from curr[j - 1]. That is the cell to the left, not the diagonal. One character of text1 can then match twice. longest_common_subsequence("ab", "bb") returns 2 instead of 1.
  • Use 0 for every base cell. For the longest common subsequence the empty-prefix cells are 0. For Edit Distance (LC 72) they are i and j. With zero base cells, "horse" to "ros" gives 2 instead of 3.
  • Drop the match case. With only max(prev[j], curr[j - 1]), no cell ever grows, and the answer is always 0.
  • Try to rebuild the answer from rolling rows. The earlier rows are gone. Keep the full table when the problem asks for the sequence or the edit steps.

Practice

Canonical LeetCode:Longest Common Subsequence (LC 1143)Edit Distance (LC 72)Delete Operation for Two Strings (LC 583)Interleaving String (LC 97)Regular Expression Matching (LC 10)Wildcard Matching (LC 44)Minimum Window Subsequence (LC 727)
Theory Context•Dynamic Programming

Builds on Grid DP, which you haven’t finished yet.

P1MediumBuilt on Grid DP~4 min speedrun

Two-Sequence DP (Longest Common Subsequence, Edit Distance)

Built on
Grid DPThe two axes of the table are the prefixes of two sequences, and a cell also looks at the cell diagonally up and to the left.
Grows into
Distinct Subsequences
The problem

Longest Common Subsequence (LC 1143)(opens leetcode.com)

You're handed two strings, text1 and text2. A subsequence of a string is whatever remains after deleting zero or more of its characters while keeping the survivors in their original order -- for instance, "ace" can be pulled out of "abcde" this way. A common subsequence is a subsequence the two strings share.

Report the length of the longest subsequence they have in common, or 0 if they share none.

Example 1
Input:text1 = "abcde", text2 = "ace"
Output:3
Both strings share the subsequence "ace", giving a length of 3.

Two-Sequence DP

dp[i][j] = the answer for text1[:i] and text2[:j] ➔ built from the cell above, the cell to the left and the diagonal ➔ 1D rolling row
Spot it when: Two strings or arrays are compared or transformed (common subsequence, edit distance, wildcard matching), and the answer for prefixes of both is built from answers for shorter prefixes.
Two-Sequence DP
Python
1def longest_common_subsequence(text1: str, text2: str) -> int:
2 """Longest Common Subsequence with two rolling rows.
3 Time: O(M * N) | Space: O(min(M, N))
4 """
5 if len(text1) < len(text2):
6 text1, text2 = text2, text1 # text2 is the shorter string
7 
8 m, n = len(text1), len(text2)
9 # prev, curr: two rolling rows of the DP table; index 0 of each row is the empty prefix
10 prev = [0] * (n + 1)
11 
12 for i in range(1, m + 1):
13 curr = [0] * (n + 1)
14 for j in range(1, n + 1):
15 # 1. Extend Or Drop One Character:
16 # A match extends the diagonal (prev[j - 1] + 1); otherwise drop one of the two last characters.
17 if text1[i - 1] == text2[j - 1]:
18 curr[j] = prev[j - 1] + 1
19 else:
20 curr[j] = max(prev[j], curr[j - 1])
21 # 2. Roll The Row Forward:
22 # curr becomes the finished row i; it is read as the row above while filling row i + 1.
23 prev = curr
24 
25 return prev[n]
Invariant: After row i is filled, prev[j] is the answer for text1[:i] and text2[:j] for every j.

Let the two axes of the table be the prefixes text1[:i] and text2[:j]. Cell (i, j) depends on three neighbours: the cell above (drop the last character of text1), the cell to the left (drop the last character of text2) and the diagonal cell (use both last characters when they match). Rows are filled in order, so all three are final, and two rolling rows are enough. The trap is the index: row i ends with text1[i - 1], so compare text1[i - 1] == text2[j - 1]; comparing text1[i] reads one character too far.

See it run

Longest Common Subsequence, step by step

i - · j - · curr[j] -

i - · j - · curr[j] -

i 1 · j 1 · curr[j] 1

i 1 · j 2 · curr[j] 1

i 1 · j 3 · curr[j] 1

i 2 · j 1 · curr[j] 1

i 2 · j 2 · curr[j] 1

i 2 · j 3 · curr[j] 1

i 3 · j 1 · curr[j] 1

i 3 · j 2 · curr[j] 2

i 3 · j 3 · curr[j] 2

i 4 · j 1 · curr[j] 1

i 4 · j 2 · curr[j] 2

i 4 · j 3 · curr[j] 2

i 5 · j 1 · curr[j] 1

i 5 · j 2 · curr[j] 2

i 5 · j 3 · curr[j] 3

prev[n] 3

Row i is the prefix text1[:i]. Column j is the prefix text2[:j]. A cell is the longest common subsequence length of those two prefixes.

  • new the cell being written
  • diag the cell one row up and one column left, read on a match
  • above the cell one row up, read on a mismatch
  • left the cell one column left, read on a mismatch
  • answer the final answer

Start: text1 = "abcde" and text2 = "ace". Row i is the prefix text1[:i]. Column j is the prefix text2[:j].

prev = [0] * (n + 1) is row 0, and curr[0] = 0 starts every row. An empty prefix shares no letters with any prefix, so these cells are 0.

i = 1, j = 1: text1[0] = "a" equals text2[0] = "a", so curr[1] = prev[0] + 1 = 0 + 1 = 1.

i = 1, j = 2: text1[0] = "a" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(0, 1) = 1.

i = 1, j = 3: text1[0] = "a" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(0, 1) = 1.

i = 2, j = 1: text1[1] = "b" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 2, j = 2: text1[1] = "b" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(1, 1) = 1.

i = 2, j = 3: text1[1] = "b" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(1, 1) = 1.

i = 3, j = 1: text1[2] = "c" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 3, j = 2: text1[2] = "c" equals text2[1] = "c", so curr[2] = prev[1] + 1 = 1 + 1 = 2.

i = 3, j = 3: text1[2] = "c" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(1, 2) = 2.

i = 4, j = 1: text1[3] = "d" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 4, j = 2: text1[3] = "d" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(2, 1) = 2.

i = 4, j = 3: text1[3] = "d" and text2[2] = "e" differ, so curr[3] = max(prev[3], curr[2]) = max(2, 2) = 2.

i = 5, j = 1: text1[4] = "e" and text2[0] = "a" differ, so curr[1] = max(prev[1], curr[0]) = max(1, 0) = 1.

i = 5, j = 2: text1[4] = "e" and text2[1] = "c" differ, so curr[2] = max(prev[2], curr[1]) = max(2, 1) = 2.

i = 5, j = 3: text1[4] = "e" equals text2[2] = "e", so curr[3] = prev[2] + 1 = 2 + 1 = 3.

The loops end. prev[3] = 3, the length of the longest common subsequence of "abcde" and "ace".

empty
empty
a
ac
ace
a
ab
abc
abcd
abcde
1 / 18
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Comparing text1[i] and text2[j] without the - 1

    Mistake
    if text1[i] == text2[j]:
    Fix
    if text1[i - 1] == text2[j - 1]:

    What happens: The table has an extra row and column for the empty prefix. So row i stands for the prefix text1[:i], whose last character is text1[i - 1]. In the same way, column j ends with text2[j - 1]. text1[i] and text2[j] are one character too far. At j = n, text2[j] reads past the end of text2.

    Example: text1 = "abc", text2 = "ab"
    Mistake: raises IndexErrorFix: returns 2
  2. 2

    Gotcha 2: Reading the diagonal from curr[j - 1] instead of prev[j - 1]

    Mistake
    curr[j] = curr[j - 1] + 1
    Fix
    curr[j] = prev[j - 1] + 1

    What happens: A match extends the diagonal cell dp[i-1][j-1]. With two rolling rows, that cell is prev[j - 1]. curr[j - 1] is the cell to the left in the current row, so one character of text1 can match twice. With a single 1D row, save the diagonal in a temporary variable before you overwrite it.

    Example: text1 = "ab", text2 = "bb"
    Mistake: returns 2Fix: returns 1
  3. 3

    Gotcha 3: Using 0 for every empty-prefix base cell

    Mistake
    dp[i][0] = 0
    dp[0][j] = 0
    Fix
    dp[i][0] = i
    dp[0][j] = j

    What happens: The empty-prefix row and column hold base cases, and their values depend on the problem. For the LCS, they are 0. In Edit Distance (LC 72), turning word1[:i] into the empty string takes i deletions. So dp[i][0] = i and dp[0][j] = j.

    Example: Edit Distance (LC 72): word1 = "horse", word2 = "ros"
    Mistake: returns 2Fix: returns 3
Not this one if…
  • You walk right and down through one grid and no second sequence is compared, as in Unique Paths (LC 62): each cell needs only the cell above and the cell to the left. See Grid DP
  • You must count how many different ways one string can be picked out of the other, as in Distinct Subsequences (LC 115), not the length or cost of one best alignment. See Distinct Subsequences

Deep Dive

What a cell means when the axes are two strings

Grid DP already taught the table and the row-by-row fill. This card changes what the two axes are. Row i stands for the prefix text1[:i], and column j stands for the prefix text2[:j]. A prefix is the first characters of a string.

The cell (i, j) is the answer for that pair of prefixes. In longest_common_subsequence, it is the length of the longest common subsequence of text1[:i] and text2[:j]. A subsequence keeps the order of characters but may skip characters.

Index 0 on each axis is the empty prefix. These extra cells let the first real character use the same formula as every other character. The number of states is (m + 1) * (n + 1).

Why the recurrence is correct

Look at the last characters text1[i - 1] and text2[j - 1]. If they are equal, some best common subsequence can end with that pair. Taking the pair never loses length, so the answer is prev[j - 1] + 1. The cell on the diagonal is the answer for the two prefixes without those last characters.

If they differ, they cannot both be the last matched pair. At least one of them is not used. So the answer is the better of dropping the last character of text1, which is prev[j], or dropping the last character of text2, which is curr[j - 1].

Example: for text1 = "abcde" and text2 = "ace", the final row is [0, 1, 2, 3], so the answer is 3, the subsequence "ace". If the match case is removed and every cell uses max(prev[j], curr[j - 1]), all cells stay 0, and "abc" against "abc" returns 0 instead of 3.

Why two rows are needed, and what is lost

A match reads the diagonal cell prev[j - 1]. In a single row, row[j - 1] is overwritten by the current row before it is needed as the diagonal. So the code keeps two rows, prev and curr. After the inner loop, prev = curr makes the finished row the row above for the next i.

The code swaps the strings so that text2 is the shorter one. The length of the longest common subsequence does not change when the strings swap places. So each row has min(m, n) + 1 entries.

Rolling rows keep only the last row. You can read the length, but you cannot walk back through the table to print a subsequence. For that, keep the full table.

The cost, and the same shape for Edit Distance

Each cell reads at most three cells and does one comparison. The time is O(M * N). For two strings of length 1000, that is about one million cell updates. The space is O(min(M, N)) for the two rows.

Edit Distance (LC 72) uses the same table. The base cells are not 0. Turning word1[:i] into the empty string takes i deletions, so dp[i][0] = i and dp[0][j] = j. On a match the cell copies the diagonal. On a mismatch it is 1 + min(above, left, diagonal), which stands for a delete, an insert, or a replace. For "horse" and "ros" the answer is 3.

So the work to learn is the meaning of the three neighbours and the base cells. The loops stay the same.

60-Second Pitch

"A brute force tries every subsequence of one string and checks it against the other, which takes exponential time. I define one cell (i, j) as the answer for the first i characters of one string and the first j characters of the other. Each cell depends on the cell above, the cell to the left, and the diagonal cell. If the last characters match, I take the diagonal value plus one. If they differ, I take the larger of the cell above and the cell to the left. There are M times N cells with constant work each, so the time is O(M * N). I keep only two rows of the shorter string's length, so the space is O(min(M, N)). If the problem needs the actual subsequence, I keep the full table and walk back from the last cell."

Common Questions

How do you print the actual longest common subsequence, not only its length?

Keep the full table, with M + 1 rows and N + 1 columns. Start at the last cell. If the last characters match, add that character to the result and move to the diagonal cell. If not, move to the neighbour with the larger value, the cell above or the cell to the left. Reverse the collected characters at the end. This uses O(M * N) space.

What are the time and space complexity of longest_common_subsequence?

O(M * N) time, because each of the (M + 1) * (N + 1) cells does constant work. The space is O(min(M, N)), because the code keeps two rows of the shorter string's length plus one.

Why does the code need two rows when Unique Paths (LC 62) needs one?

A match reads the diagonal cell prev[j - 1]. In one row, that cell has already been overwritten by the current row. Unique Paths reads only the cell above and the cell to the left, so one row is enough there.

How does Edit Distance (LC 72) differ from this table?

The grid and the three neighbours are the same. The base cells are i and j, because turning a prefix into the empty string costs one deletion per character. On a match the cell copies the diagonal. On a mismatch it is 1 + min(above, left, diagonal). It takes a minimum instead of a maximum.

How does Delete Operation for Two Strings (LC 583) use the same table?

Only the characters in a common subsequence can stay, and every other character is deleted. So the answer is len(text1) + len(text2) - 2 * lcs, where lcs is the result of longest_common_subsequence. For "sea" and "eat" the longest common subsequence has length 2, so the answer is 2.

SWE Reasoning

Spot it

  • Two strings or two arrays are compared, aligned, or turned into each other: common subsequence, edit distance, wildcard or pattern matching.
  • The answer for a pair of prefixes (i, j) can be built from the answers for (i - 1, j), (i, j - 1) and (i - 1, j - 1).
  • The sizes are about 1000 or less on each side, so M * N cells is fine, and trying every subsequence is exponential.
  • The problem asks for one number (a length, a count, a minimum cost), not for every solution listed.
  • Counting the number of different ways, as in Distinct Subsequences (LC 115), also fits this table, with a sum in place of a maximum.

The invariant, step by step

  1. Before the loop, prev is all zeros. It describes the empty prefix of text1, which has no common subsequence with any prefix of text2.
  2. At row i, curr[0] is 0, because an empty prefix of text2 matches nothing.
  3. For each j, the code compares text1[i - 1] with text2[j - 1]. These are the last characters of the two prefixes.
  4. On a match, curr[j] = prev[j - 1] + 1. The pair extends the best answer for the two shorter prefixes.
  5. On a mismatch, curr[j] = max(prev[j], curr[j - 1]). One of the two last characters is dropped.
  6. After the inner loop, prev = curr. Now prev[j] is the answer for text1[:i] and text2[:j] for every j, which is the card's invariant. The last step returns prev[n].

Traps

  • Compare text1[i] with text2[j]. The last character of prefix i is text1[i - 1]. Without the - 1, text2[j] reads past the end at j = n and raises IndexError.
  • Read the diagonal from curr[j - 1]. That is the cell to the left, not the diagonal. One character of text1 can then match twice. longest_common_subsequence("ab", "bb") returns 2 instead of 1.
  • Use 0 for every base cell. For the longest common subsequence the empty-prefix cells are 0. For Edit Distance (LC 72) they are i and j. With zero base cells, "horse" to "ros" gives 2 instead of 3.
  • Drop the match case. With only max(prev[j], curr[j - 1]), no cell ever grows, and the answer is always 0.
  • Try to rebuild the answer from rolling rows. The earlier rows are gone. Keep the full table when the problem asks for the sequence or the edit steps.

Practice

Canonical LeetCode:Longest Common Subsequence (LC 1143)Edit Distance (LC 72)Delete Operation for Two Strings (LC 583)Interleaving String (LC 97)Regular Expression Matching (LC 10)Wildcard Matching (LC 44)Minimum Window Subsequence (LC 727)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•