Dynamic Programming: Grids & Strings
When a subproblem needs two numbers, like a grid cell or a pair of string prefixes, fill a 2D table where each cell builds on neighbors already done.
How It Works
Some subproblems take two numbers to pin down, and then the table is 2D. Two families cover most of them. In grid problems the state is a cell (r, c). In two-sequence problems the state is a pair of prefixes: dp[i][j] describes the first i characters of one string and the first j of the other.
The recipe is the same as in 1D DP: say what dp[i][j] means in one sentence, find the recurrence by asking what the last step was, set the base row and column, and fill the table in an order that has every input ready. Most 2D recurrences read some of the same three neighbors:
column j - 1 column j
+--------------+--------------+
row i - 1 | diagonal | up |
+--------------+--------------+
row i | left | dp[i][j] |
+--------------+--------------+Filling the Unique Paths Table
Step 1 of 17
dp
- Cell being filled
- Cells it reads
- Answer
dp[r][c] counts the paths from the top-left cell to (r, c) on a 3 x 7 grid, moving only right or down. The bottom-right cell, dp[2][6], will hold the answer.
All 17 steps as text
dp[r][c]counts the paths from the top-left cell to(r, c)on a3 x 7grid, moving only right or down. The bottom-right cell,dp[2][6], will hold the answer.- Row
0is all1s. Along the top edge the robot can only keep moving right, so each cell there has exactly one path. dp[1][0] = 1. The left column is an edge too: the only way down it is straight down.dp[1][1] = up + left = 1 + 1 = 2. The last move into a cell came from above or from the left. No path does both, so the two counts add up to every path.dp[1][2] = up + left = 1 + 2 = 3. Filling row by row, left to right, means the cell above was finished a row ago and the cell to the left one step ago.dp[1][3] = up + left = 1 + 3 = 4.1path arrives from above and3paths arrive from the left.dp[1][4] = up + left = 1 + 4 = 5.1path arrives from above and4paths arrive from the left.dp[1][5] = up + left = 1 + 5 = 6.1path arrives from above and5paths arrive from the left.dp[1][6] = up + left = 1 + 6 = 7.1path arrives from above and6paths arrive from the left.dp[2][0] = 1, on the left edge again. The rest of the row fills left to right, each cell reading the finished row above.dp[2][1] = up + left = 2 + 1 = 3.2paths arrive from above and1path arrives from the left.dp[2][2] = up + left = 3 + 3 = 6.3paths arrive from above and3paths arrive from the left.dp[2][3] = up + left = 4 + 6 = 10.4paths arrive from above and6paths arrive from the left.dp[2][4] = up + left = 5 + 10 = 15.5paths arrive from above and10paths arrive from the left.dp[2][5] = up + left = 6 + 15 = 21.6paths arrive from above and15paths arrive from the left.dp[2][6] = up + left = 7 + 21 = 28.7paths arrive from above and21paths arrive from the left.- The corner holds the answer:
28paths. Every one of the21cells took O(1) work, so the fill is O(m · n).
- Grid paths. A cell is entered from above or from the left, so
dp[r][c]combinesupandleft: a sum to count paths, a minimum for the cheapest path. Unique Paths and Minimum Path Sum. - Two strings. Compare the last characters of the two prefixes. A match usually moves along the diagonal, and a mismatch drops a character from one side (
uporleft) or, in Edit Distance, replaces one (diagonal). Longest Common Subsequence and Edit Distance. - Intervals.
dp[i][j]covers the substring fromitojand reads shorter intervals inside it, so you fill by increasing length instead of row by row. Longest Palindromic Subsequence is the classic.
When to Reach for It
- You move through a grid in fixed directions (usually right and down) and count paths or minimize a cost.
- Two strings or arrays are compared: a common subsequence, the edits between them, whether one interleaves or matches the other.
- The question is about a substring or subarray from
itoj, and answers for shorter ranges help. - A 1D state isn't enough, because the answer depends on a position in two things at once.
Complexity
| Operation | Time | Space |
|---|---|---|
| Grid DP over m x n cells | O(m · n) | O(m · n), or O(n) with one row |
| Two strings of lengths m and n | O(m · n) | O(m · n), or O(min(m, n)) with two rows |
| Interval DP with O(1) work per interval | O(n²) | O(n²) |
The Template
Unique Paths
A robot starts in the top-left cell of an m x n grid and can only move right or down. Count the distinct paths to the bottom-right cell (LeetCode 62). State: dp[r][c] = the number of paths from the top-left to cell (r, c).
The last move into (r, c) came either from above or from the left. Those two groups of paths don't overlap and together they cover everything, so dp[r][c] = dp[r - 1][c] + dp[r][c - 1]. The top row and the left column are all 1, because along an edge the only way is straight.
0 1 2 3 4 5 6 0 1 1 1 1 1 1 1 1 1 2 3 [4] 5 6 7 2 1 3 [6] (10) 15 21 28 (10) = up [4] + left [6]
Time O(m · n)Space O(m · n)
def unique_paths(m: int, n: int) -> int:
"""Number of paths from the top-left to the bottom-right cell of an m x n
grid, moving only right or down."""
# dp[r][c] = number of paths from the top-left to cell (r, c)
dp = [[0] * n for _ in range(m)]
for r in range(m):
for c in range(n):
if r == 0 or c == 0:
dp[r][c] = 1 # Along an edge the only way is straight.
else:
dp[r][c] = dp[r - 1][c] + dp[r][c - 1] # Arrived from above or from the left.
return dp[m - 1][n - 1]
if __name__ == "__main__":
print(unique_paths(3, 7)) # 28
print(unique_paths(3, 2)) # 3
print(unique_paths(1, 1)) # 1// Number of paths from the top-left to the bottom-right cell of an m x n
// grid, moving only right or down.
export function uniquePaths(m: number, n: number): number {
// dp[r][c] = number of paths from the top-left to cell (r, c)
const dp: number[][] = Array.from({ length: m }, () => new Array<number>(n).fill(0));
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
if (r === 0 || c === 0) dp[r][c] = 1; // Along an edge the only way is straight.
else dp[r][c] = dp[r - 1][c] + dp[r][c - 1]; // Arrived from above or from the left.
}
}
return dp[m - 1][n - 1];
}
console.log(uniquePaths(3, 7)); // 28
console.log(uniquePaths(3, 2)); // 3
console.log(uniquePaths(1, 1)); // 1package main
import "fmt"
// uniquePaths returns the number of paths from the top-left to the
// bottom-right cell of an m x n grid, moving only right or down.
func uniquePaths(m, n int) int {
// dp[r][c] = number of paths from the top-left to cell (r, c)
dp := make([][]int, m)
for r := range dp {
dp[r] = make([]int, n)
}
for r := 0; r < m; r++ {
for c := 0; c < n; c++ {
if r == 0 || c == 0 {
dp[r][c] = 1 // Along an edge the only way is straight.
} else {
dp[r][c] = dp[r-1][c] + dp[r][c-1] // Arrived from above or from the left.
}
}
}
return dp[m-1][n-1]
}
func main() {
fmt.Println(uniquePaths(3, 7)) // 28
fmt.Println(uniquePaths(3, 2)) // 3
fmt.Println(uniquePaths(1, 1)) // 1
}using System;
public class Solution
{
// Number of paths from the top-left to the bottom-right cell of an m x n
// grid, moving only right or down.
public static int UniquePaths(int m, int n)
{
// dp[r, c] = number of paths from the top-left to cell (r, c)
var dp = new int[m, n];
for (int r = 0; r < m; r++)
{
for (int c = 0; c < n; c++)
{
if (r == 0 || c == 0) dp[r, c] = 1; // Along an edge the only way is straight.
else dp[r, c] = dp[r - 1, c] + dp[r, c - 1]; // Arrived from above or from the left.
}
}
return dp[m - 1, n - 1];
}
public static void Main()
{
Console.WriteLine(UniquePaths(3, 7)); // 28
Console.WriteLine(UniquePaths(3, 2)); // 3
Console.WriteLine(UniquePaths(1, 1)); // 1
}
}public class Solution {
// Number of paths from the top-left to the bottom-right cell of an m x n
// grid, moving only right or down.
static int uniquePaths(int m, int n) {
// dp[r][c] = number of paths from the top-left to cell (r, c)
int[][] dp = new int[m][n];
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (r == 0 || c == 0) dp[r][c] = 1; // Along an edge the only way is straight.
else dp[r][c] = dp[r - 1][c] + dp[r][c - 1]; // Arrived from above or from the left.
}
}
return dp[m - 1][n - 1];
}
public static void main(String[] args) {
System.out.println(uniquePaths(3, 7)); // 28
System.out.println(uniquePaths(3, 2)); // 3
System.out.println(uniquePaths(1, 1)); // 1
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
Return the length of the longest sequence of characters that appears, in the same order, in both text1 and text2. The characters don't have to be next to each other: a subsequence is whatever is left after deleting any characters from a string without reordering the rest. Return 0 if the strings share nothing.
Example: text1 = "abcde" and text2 = "ace" returns 3, for "ace".
Show SolutionHide Solution
Approach
State: dp[i][j] = the LCS length of the first i characters of text1 and the first j of text2. The table has an extra row and column for empty prefixes, which are all 0, so dp[i][j] compares text1[i - 1] with text2[j - 1].
Look at the last character of each prefix. If they match, that character can end the common subsequence, and the rest comes from both shorter prefixes: dp[i - 1][j - 1] + 1. Taking the match is always safe, because dropping one character from a prefix lowers its LCS by at most 1, so skipping a matched pair can't do better. If they don't match, at least one of the two isn't in the answer, so drop one or the other and keep the better result: max(dp[i - 1][j], dp[i][j - 1]).
"" 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) e == e, so (3) = diagonal [2] + 1 c != e, so row c, col e = max(up 1, left 2) = 2
Filling the LCS Table
Step 1 of 18
dp
- Cell being filled
- Cells it reads
- Answer
Rows are prefixes of "abcde" and columns are prefixes of "ace". dp[i][j] is the LCS length of the first i characters of one and the first j of the other.
All 18 steps as text
- Rows are prefixes of
"abcde"and columns are prefixes of"ace".dp[i][j]is the LCS length of the firsticharacters of one and the firstjof the other. - Row
0and column0stand for an empty prefix. Nothing is common with"", so they stay0. a == a, so this pair extends the LCS of the shorter prefixes""and"", on the diagonal:dp[1][1] = 0 + 1 = 1.a != c, so at least one isn't in the LCS. Droppingaleaves0(up) and droppingcleaves1(left), sodp[1][2] = 1.a != e, so at least one isn't in the LCS. Droppingaleaves0(up) and droppingeleaves1(left), sodp[1][3] = 1.b != a, so at least one isn't in the LCS. Droppingbleaves1(up) and droppingaleaves0(left), sodp[2][1] = 1.b != c, so at least one isn't in the LCS. Droppingbleaves1(up) and droppingcleaves1(left), sodp[2][2] = 1.b != e, so at least one isn't in the LCS. Droppingbleaves1(up) and droppingeleaves1(left), sodp[2][3] = 1.c != a, so at least one isn't in the LCS. Droppingcleaves1(up) and droppingaleaves0(left), sodp[3][1] = 1.c == c, so this pair extends the LCS of the shorter prefixes"ab"and"a", on the diagonal:dp[3][2] = 1 + 1 = 2.c != e, so at least one isn't in the LCS. Droppingcleaves1(up) and droppingeleaves2(left), sodp[3][3] = 2.d != a, so at least one isn't in the LCS. Droppingdleaves1(up) and droppingaleaves0(left), sodp[4][1] = 1.d != c, so at least one isn't in the LCS. Droppingdleaves2(up) and droppingcleaves1(left), sodp[4][2] = 2.d != e, so at least one isn't in the LCS. Droppingdleaves2(up) and droppingeleaves2(left), sodp[4][3] = 2.e != a, so at least one isn't in the LCS. Droppingeleaves1(up) and droppingaleaves0(left), sodp[5][1] = 1.e != c, so at least one isn't in the LCS. Droppingeleaves2(up) and droppingcleaves1(left), sodp[5][2] = 2.e == e, so this pair extends the LCS of the shorter prefixes"abcd"and"ac", on the diagonal:dp[5][3] = 2 + 1 = 3.- The corner holds the answer:
"abcde"and"ace"share a subsequence of length3, which is all of"ace". Each cell took O(1), so the fill is O(m · n).
There are (m + 1)(n + 1) cells and each takes O(1), so time and space are both O(m · n). Each row reads only itself and the row above it, so two rows bring the space down to O(min(m, n)) if you put the shorter string along the columns.
Time O(m · n)Space O(m · n)
def longest_common_subsequence(text1: str, text2: str) -> int:
"""Length of the longest subsequence that appears in both strings."""
m, n = len(text1), len(text2)
# dp[i][j] = LCS length of the first i chars of text1 and the first j of text2.
# Row 0 and column 0 stand for an empty prefix, so they stay 0.
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] = dp[i - 1][j - 1] + 1 # Same last char: it ends the LCS of both shorter prefixes.
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # Drop the last char of one string or the other.
return dp[m][n]
if __name__ == "__main__":
print(longest_common_subsequence("abcde", "ace")) # 3
print(longest_common_subsequence("abc", "abc")) # 3
print(longest_common_subsequence("abc", "def")) # 0// Length of the longest subsequence that appears in both strings.
export function longestCommonSubsequence(text1: string, text2: string): number {
const m = text1.length;
const n = text2.length;
// dp[i][j] = LCS length of the first i chars of text1 and the first j of text2.
// Row 0 and column 0 stand for an empty prefix, so they stay 0.
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; // Same last char: it ends the LCS of both shorter prefixes.
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // Drop the last char of one string or the other.
}
}
}
return dp[m][n];
}
console.log(longestCommonSubsequence("abcde", "ace")); // 3
console.log(longestCommonSubsequence("abc", "abc")); // 3
console.log(longestCommonSubsequence("abc", "def")); // 0package main
import "fmt"
// longestCommonSubsequence returns the length of the longest subsequence that
// appears in both strings.
func longestCommonSubsequence(text1, text2 string) int {
m, n := len(text1), len(text2)
// dp[i][j] = LCS length of the first i chars of text1 and the first j of text2.
// Row 0 and column 0 stand for an empty prefix, so they stay 0.
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if text1[i-1] == text2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1 // Same last char: it ends the LCS of both shorter prefixes.
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) // Drop the last char of one string or the other.
}
}
}
return dp[m][n]
}
func main() {
fmt.Println(longestCommonSubsequence("abcde", "ace")) // 3
fmt.Println(longestCommonSubsequence("abc", "abc")) // 3
fmt.Println(longestCommonSubsequence("abc", "def")) // 0
}using System;
public class Solution
{
// Length of the longest subsequence that appears in both strings.
public static int LongestCommonSubsequence(string text1, string text2)
{
int m = text1.Length, n = text2.Length;
// dp[i, j] = LCS length of the first i chars of text1 and the first j of text2.
// Row 0 and column 0 stand for an empty prefix, so they stay 0.
var dp = new int[m + 1, n + 1];
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (text1[i - 1] == text2[j - 1])
{
dp[i, j] = dp[i - 1, j - 1] + 1; // Same last char: it ends the LCS of both shorter prefixes.
}
else
{
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]); // Drop the last char of one string or the other.
}
}
}
return dp[m, n];
}
public static void Main()
{
Console.WriteLine(LongestCommonSubsequence("abcde", "ace")); // 3
Console.WriteLine(LongestCommonSubsequence("abc", "abc")); // 3
Console.WriteLine(LongestCommonSubsequence("abc", "def")); // 0
}
}public class Solution {
// Length of the longest subsequence that appears in both strings.
static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
// dp[i][j] = LCS length of the first i chars of text1 and the first j of text2.
// Row 0 and column 0 stand for an empty prefix, so they stay 0.
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1; // Same last char: it ends the LCS of both shorter prefixes.
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // Drop the last char of one string or the other.
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println(longestCommonSubsequence("abcde", "ace")); // 3
System.out.println(longestCommonSubsequence("abc", "abc")); // 3
System.out.println(longestCommonSubsequence("abc", "def")); // 0
}
}Problem
You can change a word with three operations: insert a character, delete a character, or replace one character with another. Return the fewest operations that turn word1 into word2.
Example: word1 = "horse" and word2 = "ros" returns 3. Replace the h with r to get rorse, delete the second r to get rose, then delete the e to get ros.
Show SolutionHide Solution
Approach
State: dp[i][j] = the fewest edits that turn the first i characters of word1 into the first j characters of word2. The base row and column aren't zero this time. Turning i characters into nothing takes i deletes, and building j characters from nothing takes j inserts.
If the last characters match, they cost nothing and dp[i][j] = dp[i - 1][j - 1]. If they don't, the last edit was one of three, and each one leaves a smaller state that's already in the table:
+-------------------+-------------------+ | dp[i-1][j-1] | dp[i-1][j] | | replace | delete word1[i-1] | +-------------------+-------------------+ | dp[i][j-1] | dp[i][j] | | insert word2[j-1] | | +-------------------+-------------------+
- Replace
word1[i - 1]withword2[j - 1]. Both last characters are now settled, leavingdp[i - 1][j - 1]. - Delete
word1[i - 1]. The remainingi - 1characters still have to become allj, leavingdp[i - 1][j]. - Insert
word2[j - 1]at the end. That settles the last target character, so allicharacters have to become the firstj - 1, leavingdp[i][j - 1].
"" r o s "" 0 1 2 3 h 1 1 2 3 o 2 2 1 2 r 3 2 2 2 s 4 3 [3] [2] e 5 4 [4] (3) e != s, so (3) = 1 + min(diagonal 3, up 2, left 4) s == s, so row s, col s = diagonal 2, with no edit
Time O(m · n)Space O(m · n)
def edit_distance(word1: str, word2: str) -> int:
"""Fewest single-character inserts, deletes or replacements that turn word1
into word2."""
m, n = len(word1), len(word2)
# dp[i][j] = edits to turn the first i chars of word1 into the first j of word2.
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i # Delete all i chars.
for j in range(n + 1):
dp[0][j] = j # Insert all j chars.
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] # Last chars already match: no edit needed.
else:
replace = dp[i - 1][j - 1] # Turn word1's last char into word2's.
remove = dp[i - 1][j] # Delete word1's last char.
insert = dp[i][j - 1] # Append word2's last char.
dp[i][j] = 1 + min(replace, remove, insert)
return dp[m][n]
if __name__ == "__main__":
print(edit_distance("horse", "ros")) # 3
print(edit_distance("intention", "execution")) # 5
print(edit_distance("", "abc")) # 3// Fewest single-character inserts, deletes or replacements that turn word1
// into word2.
export function editDistance(word1: string, word2: string): number {
const m = word1.length;
const n = word2.length;
// dp[i][j] = edits to turn the first i chars of word1 into the first j of word2.
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i; // Delete all i chars.
for (let j = 0; j <= n; j++) dp[0][j] = j; // Insert all j chars.
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]; // Last chars already match: no edit needed.
} else {
const replace = dp[i - 1][j - 1]; // Turn word1's last char into word2's.
const remove = dp[i - 1][j]; // Delete word1's last char.
const insert = dp[i][j - 1]; // Append word2's last char.
dp[i][j] = 1 + Math.min(replace, remove, insert);
}
}
}
return dp[m][n];
}
console.log(editDistance("horse", "ros")); // 3
console.log(editDistance("intention", "execution")); // 5
console.log(editDistance("", "abc")); // 3package main
import "fmt"
// editDistance returns the fewest single-character inserts, deletes or
// replacements that turn word1 into word2.
func editDistance(word1, word2 string) int {
m, n := len(word1), len(word2)
// dp[i][j] = edits to turn the first i chars of word1 into the first j of word2.
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 0; i <= m; i++ {
dp[i][0] = i // Delete all i chars.
}
for j := 0; j <= n; j++ {
dp[0][j] = j // Insert all j chars.
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1] // Last chars already match: no edit needed.
} else {
replace := dp[i-1][j-1] // Turn word1's last char into word2's.
remove := dp[i-1][j] // Delete word1's last char.
insert := dp[i][j-1] // Append word2's last char.
dp[i][j] = 1 + min(replace, remove, insert)
}
}
}
return dp[m][n]
}
func main() {
fmt.Println(editDistance("horse", "ros")) // 3
fmt.Println(editDistance("intention", "execution")) // 5
fmt.Println(editDistance("", "abc")) // 3
}using System;
public class Solution
{
// Fewest single-character inserts, deletes or replacements that turn word1
// into word2.
public static int EditDistance(string word1, string word2)
{
int m = word1.Length, n = word2.Length;
// dp[i, j] = edits to turn the first i chars of word1 into the first j of word2.
var dp = new int[m + 1, n + 1];
for (int i = 0; i <= m; i++) dp[i, 0] = i; // Delete all i chars.
for (int j = 0; j <= n; j++) dp[0, j] = j; // Insert all j chars.
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (word1[i - 1] == word2[j - 1])
{
dp[i, j] = dp[i - 1, j - 1]; // Last chars already match: no edit needed.
}
else
{
int replace = dp[i - 1, j - 1]; // Turn word1's last char into word2's.
int remove = dp[i - 1, j]; // Delete word1's last char.
int insert = dp[i, j - 1]; // Append word2's last char.
dp[i, j] = 1 + Math.Min(replace, Math.Min(remove, insert));
}
}
}
return dp[m, n];
}
public static void Main()
{
Console.WriteLine(EditDistance("horse", "ros")); // 3
Console.WriteLine(EditDistance("intention", "execution")); // 5
Console.WriteLine(EditDistance("", "abc")); // 3
}
}public class Solution {
// Fewest single-character inserts, deletes or replacements that turn word1
// into word2.
static int editDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
// dp[i][j] = edits to turn the first i chars of word1 into the first j of word2.
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) dp[i][0] = i; // Delete all i chars.
for (int j = 0; j <= n; j++) dp[0][j] = j; // Insert all j chars.
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // Last chars already match: no edit needed.
} else {
int replace = dp[i - 1][j - 1]; // Turn word1's last char into word2's.
int remove = dp[i - 1][j]; // Delete word1's last char.
int insert = dp[i][j - 1]; // Append word2's last char.
dp[i][j] = 1 + Math.min(replace, Math.min(remove, insert));
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println(editDistance("horse", "ros")); // 3
System.out.println(editDistance("intention", "execution")); // 5
System.out.println(editDistance("", "abc")); // 3
}
}Common Pitfalls
- Mixing up table indices and string indices. With an extra row and column for empty prefixes,
dp[i][j]comparestext1[i - 1]andtext2[j - 1], nottext1[i]. - Copying base cases from a different problem. LCS starts its first row and column at 0, but Edit Distance starts them at
iandj. - Shrinking to one row and overwriting a value you still need. LCS and Edit Distance read the old diagonal, so save it in a variable before you overwrite it.
- Confusing subsequence with substring. For the longest common substring, a mismatch resets the cell to 0 and the answer is the largest cell anywhere, not the corner.
- Filling the edges with 1s in a grid with obstacles. In Unique Paths II, a blocked cell on the top row or left column cuts off every edge cell after it.
Practice
More problems to try on your own.