DSA course menu
2D Dynamic Programming: Grid Paths, LCS and Edit Distance
Solve two-dimensional dynamic programming problems in Python: unique paths in a grid, minimum path sum, longest common subsequence, edit distance (Levenshtein), and rebuilding the answer from the table.
On this page
In two-dimensional DP, the state needs two numbers: a cell in a grid (row, col), or positions in two strings (i, j). The table is a grid, and each cell is computed from cells above and to the left of it, so it is filled row by row.
Unique paths in a grid#
A robot starts in the top-left corner of an m × n grid and can only move right or down. How many paths reach the bottom-right?
- State:
dp[r][c]= number of paths to cell (r, c). - Recurrence: a cell is reached from above or from the left:
dp[r][c] = dp[r-1][c] + dp[r][c-1]. - Base: the first row and first column each have exactly 1 path.
def unique_paths(m, n):
dp = [[1] * n for _ in range(m)]
for r in range(1, m):
for c in range(1, n):
dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
for row in dp:
print(row)
return dp[m - 1][n - 1]
print(unique_paths(3, 4))[1, 1, 1, 1]
[1, 2, 3, 4]
[1, 3, 6, 10]
10Minimum path sum#
Each cell has a cost. Find the cheapest path from the top-left to the bottom-right, moving right or down. Same shape, with min instead of +:
def min_path_sum(grid):
rows, cols = len(grid), len(grid[0])
dp = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
if r == 0 and c == 0:
dp[r][c] = grid[0][0]
elif r == 0:
dp[r][c] = dp[r][c - 1] + grid[r][c]
elif c == 0:
dp[r][c] = dp[r - 1][c] + grid[r][c]
else:
dp[r][c] = min(dp[r - 1][c], dp[r][c - 1]) + grid[r][c]
return dp[-1][-1]
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1],
]
print(min_path_sum(grid))7The cheapest route is 1 → 3 → 1 → 1 → 1 = 7.
Longest common subsequence (LCS)#
Find the longest sequence of characters that appears, in order, in both strings (not necessarily contiguously). LCS is behind diff tools that compare files.
- State:
dp[i][j]= LCS length of the firsticharacters ofaand the firstjcharacters ofb. - Recurrence: if
a[i-1] == b[j-1], extend:dp[i-1][j-1] + 1. Otherwise drop a character from one string:max(dp[i-1][j], dp[i][j-1]).
def lcs(a, b):
dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 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])
# walk back through the table to rebuild one LCS
i, j, chars = len(a), len(b), []
while i and j:
if a[i - 1] == b[j - 1]:
chars.append(a[i - 1]); i -= 1; j -= 1
elif dp[i - 1][j] >= dp[i][j - 1]:
i -= 1
else:
j -= 1
return dp[len(a)][len(b)], "".join(reversed(chars))
print(lcs("ABCBDAB", "BDCABA"))
print(lcs("programming", "gaming"))(4, 'BCBA')
(6, 'gaming')The extra row and column of zeros represent empty prefixes, so no special base cases are needed. Time and space are O(m × n).
Edit distance#
The fewest single-character insertions, deletions or replacements to turn one word into another (also called Levenshtein distance). Spell checkers and DNA comparison use it.
- State:
dp[i][j]= edit distance between the firsticharacters ofaand the firstjofb. - Base: turning a prefix into the empty string takes
ideletions; building a prefix from empty takesjinsertions. - Recurrence: if the last characters match,
dp[i-1][j-1]. Otherwise 1 + the cheapest of delete (dp[i-1][j]), insert (dp[i][j-1]) or replace (dp[i-1][j-1]).
def edit_distance(a, b):
dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(len(a) + 1):
dp[i][0] = i
for j in range(len(b) + 1):
dp[0][j] = j
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 1):
if a[i - 1] == b[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j], # delete
dp[i][j - 1], # insert
dp[i - 1][j - 1]) # replace
return dp[len(a)][len(b)]
print(edit_distance("kitten", "sitting"))
print(edit_distance("horse", "ros"))
print(edit_distance("", "abc"))3
3
3kitten → sitten (replace k) → sittin (replace e) → sitting (insert g): 3 edits.
Saving space#
Each row only uses the row above it, so two rows of length n are enough, cutting space from O(m × n) to O(n). The cost is that you can no longer walk back through the table to rebuild the actual sequence, as lcs did.
Common mistakes#
Practice#
Practice exercise
Write longest_palindromic_subsequence(s), the length of the longest subsequence of s that reads the same both ways.
Show hint
A palindrome read backwards is itself, so the answer is the LCS of s and s[::-1].
Show solution
def lcs_length(a, b):
dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 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])
return dp[-1][-1]
def longest_palindromic_subsequence(s):
return lcs_length(s, s[::-1])
print(longest_palindromic_subsequence("bbbab"))
print(longest_palindromic_subsequence("character"))4
5"bbbb" (length 4) is hidden in "bbbab", and "carac" (length 5) in "character".
Key takeaway#
Next: choosing items under a limit, in The Knapsack Problem.
Finished this lesson?