Dynamic Programming II: Grids & Strings
Two-dimensional state: grid paths, edit distance, and the knapsack family. Define the state, then fill the table.
Some problems need more than one coordinate to describe the state. In grid DP, you need a row and a column. In string DP, you often need positions in two strings. In knapsack, you need which items are available and how much capacity remains.
The move from 1-D to 2-D does not change the discipline: define the state, write the recurrence, set the base cases, and fill in an order that respects the dependencies. The table is bigger, but the thinking is the same.
String DP deserves special attention because many hard-looking interview problems are just prefix comparisons: “best answer using the first i characters of one string and the first j characters of another.”
The mental model
A 2-D state usually means one of these:
dp[r][c]— answer for a grid cell.dp[i][j]— answer for prefixestext1[:i]andtext2[:j].dp[i][cap]— answer using the firstiitems with capacitycap.
For grid paths that only move down or right, each cell depends on the cell above and the cell to the left:
for r in range(rows):
for c in range(cols):
dp[r][c] = ways_from_top + ways_from_left
For two-string prefix DP, the extra zero row and zero column make base cases clean. They represent comparing against an empty prefix:
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] = 1 + dp[i - 1][j - 1]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
The fill order is not decorative. If a cell reads top, left, and diagonal, you must fill rows left-to-right, top-to-bottom or use an equivalent order where those values already exist.
Pattern recognition
Variations
Worked problems
Try each yourself before revealing the solution. Pay attention to what each table cell means before reading the code.
Unique Paths
A robot starts in the top-left cell of an m by n grid and can move only right or down. Return how many different paths reach the bottom-right cell.
Approach. Let dp[r][c] be the number of ways to reach cell (r, c). The robot can only arrive from above or from the left, so the recurrence is dp[r][c] = dp[r - 1][c] + dp[r][c - 1]. The first row and first column are all
- A single row array is enough because
row[c]is the old top value androw[c - 1]is the current left value.
Show solution
def unique_paths(m, n):
row = [1] * n
for _ in range(1, m):
for c in range(1, n):
row[c] += row[c - 1]
return row[-1]Complexity. O(mn) time and O(n) space. The full 2-D recurrence is preserved, but only one row is stored.
Longest Common Subsequence
Given two strings, return the length of the longest sequence of characters that appears in both strings in the same relative order. Characters do not need to be contiguous.
Approach. Let dp[i][j] be the LCS length for text1[:i] and text2[:j]. If the newest characters match, they can extend the best answer for the two smaller prefixes. If they differ, drop one newest character and keep the better of the two possibilities.
Show solution
def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = 1 + dp[i - 1][j - 1]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]Complexity. O(mn) time and O(mn) space. The table has one cell for every pair of prefix lengths.
Edit Distance
Given two words, return the minimum number of single-character inserts, deletes, or replacements needed to transform the first word into the second.
Approach. Let dp[i][j] be the minimum edits to transform word1[:i] into word2[:j]. If the newest characters match, no new edit is needed. Otherwise, try the three possible final edits: delete from word1, insert into word1, or replace the newest character. Only the previous row and current row are needed.
Show solution
def min_distance(word1, word2):
m, n = len(word1), len(word2)
prev = list(range(n + 1))
for i in range(1, m + 1):
curr = [i] + [0] * n
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
curr[j] = prev[j - 1]
else:
delete = prev[j]
insert = curr[j - 1]
replace = prev[j - 1]
curr[j] = 1 + min(delete, insert, replace)
prev = curr
return prev[n]Complexity. O(mn) time and O(n) space. A full table is O(mn) space; rolling rows are safe because each cell only needs the previous row and the current row’s left neighbor.
The fill-order pitfall
When a prompt compares two prefixes, imagine the table before you code. Label the empty row and column, say what a diagonal move means, and only then choose whether the final implementation needs the whole table or just a rolling row.