Expert: advanced patterns and interviews · Dynamic programming · topic 54 of 61 · 40 min
2-D DP on grids and strings
Explanation
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).
| "" | a | c | e | |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| b | 0 | 1 | 1 | 1 |
| c | 0 | 1 | 2 | 2 |
| d | 0 | 1 | 2 | 2 |
| e | 0 | 1 | 2 | 3 |
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.
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: aceEdit 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
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.
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))Practice (0/4 done)
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.
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.
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.
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Knapsack problemsDynamic programming · same module
- Sliding window: variable sizeHashing, two pointers and sliding window
- Breadth-first search (BFS)Graphs
- CSS GridFrontend Web Development · CSS layout and responsive design
- Memoization: remembering answersRecursion and backtracking
- How indexes workSQL and Databases · Indexes, query plans and views
Questions people ask about this
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.
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.
grid has 3 rows and 5 columns. What is len(grid[0])?Related
What is the time of the coin-change-minimum DP with amount A and k coin values?Related
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
- 2 more topics in “Dynamic programming” before its exam: Knapsack problems, More DP patterns and how to spot them.
- Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.