Skip to content

Expert: advanced patterns and interviews · Dynamic programming · topic 54 of 61 · 40 min

2-D DP on grids and strings

Explanation

Some problems need two numbers to describe a smaller problem: a cell (row, column) in a grid, or a position in each of two strings. Then the DP table is a grid, and each cell is built from its neighbours above, to the left, or diagonally. Unique paths, minimum path sum, longest common subsequence and edit distance all follow this shape, and you can walk back through the table to rebuild the actual answer.

When one index is not enough

In 1-D DP, one number (a position or an amount) described each smaller problem. Some problems need two. "How many ways can I reach cell (r, c)?" needs a row and a column. "How alike are the first i letters of one word and the first j letters of another?" needs i and j. The table becomes a grid, dp[i][j], and the four-step recipe stays the same.

Unique paths in a grid, with obstacles

A robot starts at the top-left cell and may only move right or down. Some cells are blocked (marked 1). How many different paths reach the bottom-right cell? State: dp[r][c] = the number of paths from the start to cell (r, c). The last move into (r, c) came from above or from the left, so dp[r][c] = dp[r − 1][c] + dp[r][c − 1], and a blocked cell has dp = 0. Base case: dp[0][0] = 1 if the start is free. Fill row by row, left to right, so the cells above and to the left are always ready.

Each row only reads the row above it, so one list of length n is enough: dp[c] still holds the value from the row above until you overwrite it, and dp[c − 1] already holds the new value from the left. Time O(m × n), space O(n).

Minimum path sum

Same moves, but each cell has a cost and you want the cheapest path. dp[r][c] = grid[r][c] + min(dp[r − 1][c], dp[r][c − 1]). The first row can only be reached from the left and the first column only from above, so fill those as running sums first. For [[1, 3, 1], [1, 5, 1], [4, 2, 1]] the answer is 7, along 1 → 3 → 1 → 1 → 1.

Longest common subsequence (LCS)

A subsequence keeps some characters of a string in their original order, not necessarily next to each other: "ace" is a subsequence of "abcde", "aec" is not. The LCS of two strings is the longest string that is a subsequence of both. State: dp[i][j] = the LCS length of the first i characters of a and the first j characters of b. If a[i − 1] == b[j − 1], that character can end the common subsequence: dp[i][j] = dp[i − 1][j − 1] + 1. Otherwise one of the two last characters is not used: dp[i][j] = max(dp[i − 1][j], dp[i][j − 1]). Row 0 and column 0 are 0 (an empty string has nothing in common).

""ace
""0000
a0111
b0111
c0122
d0122
e0123

The table above is for a = "abcde" (rows) and b = "ace" (columns). The answer, 3, is in the bottom-right cell.

Rebuilding the answer from the table

The table stores lengths, not the string, but you can walk back from the bottom-right cell. If the two characters match, that character is in the LCS: take it and move diagonally up-left. Otherwise move to whichever neighbour (up or left) holds the larger value. Collect characters in a list and reverse it at the end. The same walk-back idea rebuilds paths in grids and the list of edits in edit distance.

python
def lcs_string(a, b):
    m, n = len(a), len(b)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            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])
    out, i, j = [], m, n
    while i > 0 and j > 0:
        if a[i - 1] == b[j - 1]:
            out.append(a[i - 1])
            i, j = i - 1, j - 1
        elif dp[i - 1][j] >= dp[i][j - 1]:
            i -= 1
        else:
            j -= 1
    return "".join(reversed(out))

print(lcs_string("abcde", "ace"))   # Output: ace
O(m × n) time and space. Keep the full table when you need to rebuild the answer.

Edit distance

The edit distance (Levenshtein distance) between two words is the fewest single-character edits (insert, delete or replace) that turn one into the other. "horse" becomes "ros" in 3 edits: replace h with r, delete r, delete e. State: dp[i][j] = the edits to turn the first i characters of a into the first j characters of b. Base cases: dp[i][0] = i (delete everything) and dp[0][j] = j (insert everything). If the last characters match, dp[i][j] = dp[i − 1][j − 1]. Otherwise 1 + the smallest of: dp[i − 1][j] (delete from a), dp[i][j − 1] (insert into a) and dp[i − 1][j − 1] (replace). Time O(m × n); two rows give O(n) space when you only need the number.

Common mistakes

  • Building the grid with [[0] * n] * m, which makes every row the same list. Fix: [[0] * n for _ in range(m)].
  • Forgetting the extra row and column for the empty prefix in string DP, then reading dp[−1] by accident. Fix: make the table (m + 1) × (n + 1) and use a[i − 1] for the i-th character.
  • In unique paths, setting the whole first row to 1 even after an obstacle. Fix: once a cell in the first row or column is blocked, every cell after it in that row or column is 0; the general rule handles it if you start from dp[0][0].
  • Confusing subsequence with substring. A substring must be continuous; LCS allows gaps. Fix: read the question and test "abcde" with "ace".
  • Mixing up insert and delete in edit distance. Both cost 1, so the number is the same, but the walk-back changes. Fix: write what each neighbour means as a comment.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

2-D DP on grids and strings · 1/8

2-D DP

This lesson moves DP into two dimensions: grids, and pairs of strings.

Prefer a video by a teacher? Find free videos on “longest common subsequence edit distance unique paths 2d dynamic programming” on YouTube ↗

In real life: Spell-check and autocorrect suggestions

When you type "definately" in a document or a phone keyboard, the checker compares the word with dictionary words and suggests the ones with the smallest edit distance. Real keyboards also weigh how often each word is used, which keys sit next to each other, and swapped letters, but edit distance is the classic core. The same LCS-style table is behind "diff" tools that show which lines changed between two versions of a file.

python
def edit_distance(a, b):
    prev = list(range(len(b) + 1))
    for i in range(1, len(a) + 1):
        cur = [i] + [0] * len(b)
        for j in range(1, len(b) + 1):
            if a[i - 1] == b[j - 1]:
                cur[j] = prev[j - 1]
            else:
                cur[j] = 1 + min(prev[j], cur[j - 1], prev[j - 1])
        prev = cur
    return prev[len(b)]

typed = "definately"
words = ["definitely", "defiantly", "delicately", "finally"]
for w in sorted(words, key=lambda w: edit_distance(typed, w)):
    print(w, edit_distance(typed, w))
Shows definitely 1, delicately 2, defiantly 3, finally 4: "definitely" is the best suggestion.

Practice (0/4 done)

Task 1 · Code · Python

Write unique_paths_with_obstacles(grid). grid is a list of rows; 0 is a free cell and 1 is blocked. Starting at the top-left cell and moving only right or down, return how many different paths reach the bottom-right cell. If the start or the end is blocked, return 0. Return 0 for [] or [[]]. Example: [[0, 0, 0], [0, 1, 0], [0, 0, 0]] returns 2.

Task 2 · Code · Python

Write min_path_sum(grid) for a grid of non-negative numbers. Moving only right or down from the top-left to the bottom-right cell, return the smallest possible sum of the cells on the path (both ends included). Return 0 for [] or [[]]. Example: [[1, 3, 1], [1, 5, 1], [4, 2, 1]] returns 7.

Task 3 · Code · Python

Write lcs_length(a, b) that returns the length of the longest common subsequence of strings a and b (characters in the same order, gaps allowed). O(len(a) × len(b)) time. Example: lcs_length("abcde", "ace") returns 3.

Task 4 · Code · Python

Write edit_distance(a, b) that returns the fewest single-character inserts, deletes or replaces needed to turn string a into string b. O(len(a) × len(b)) time. Example: edit_distance("horse", "ros") returns 3.

Quick quiz

Question 1

1.In "unique paths" (moves right or down only), where can the paths into cell (r, c) come from?

Question 2

2.In LCS, a[i − 1] != b[j − 1]. What is dp[i][j]?

Question 3

3.Which is a subsequence of "programming" but NOT a substring?

Question 4

4.In edit distance, why is dp[i][0] = i?

Question 5

5.You only need the LCS LENGTH of two strings of lengths m and n. What is the smallest space you need with the usual DP?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

In "unique paths" (moves right or down only), where can the paths into cell (r, c) come from?

Answer: From the cell above and the cell to the left

With only right and down moves, the last step into (r, c) was down from (r − 1, c) or right from (r, c − 1).

In LCS, a[i − 1] != b[j − 1]. What is dp[i][j]?

Answer: max(dp[i − 1][j], dp[i][j − 1])

At least one of the two last characters is not in the common subsequence, so drop one or the other and keep the better result.

Which is a subsequence of "programming" but NOT a substring?

Answer: "pgm"

p, g, m appear in that order with gaps; the other three appear as continuous runs.

In edit distance, why is dp[i][0] = i?

Answer: Turning i characters into an empty string needs i deletes

The only way to reach the empty string is to delete every one of the i characters.

You only need the LCS LENGTH of two strings of lengths m and n. What is the smallest space you need with the usual DP?

Answer: O(min(m, n)) using two rows

Each row reads only the row above, so two rows of the shorter length are enough. To rebuild the string you need the full table (or a cleverer method).

A question says "n ≤ 15 tasks, assign each worker a different task, minimise the total cost". Which DP pattern fits?Related

Answer: Bitmask DP over the set of used tasks

The state needs to know which tasks are already used; with n ≤ 15 there are only 2¹⁵ = 32,768 sets.

From More DP patterns and how to spot them

What is a main advantage of bottom-up DP over top-down in Python?Related

Answer: It avoids the recursion limit and makes space saving easy

A loop has no call stack to overflow, and the fixed filling order lets you keep only the last values.

From Dynamic programming: the idea

grid has 3 rows and 5 columns. What is len(grid[0])?Related

Answer: 5

grid[0] is the first row, which holds one value per column.

From 2-D arrays (matrices)

What is the time of the coin-change-minimum DP with amount A and k coin values?Related

Answer: O(A × k)

There are A + 1 states, and each tries k coins.

From 1-D DP classics

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Dynamic programming” before its exam: Knapsack problems, More DP patterns and how to spot them.
  2. Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.

← Previous: 1-D DP classics