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.

AdvancedRead first: Dynamic Programming: 1D

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 row by row, left to right, finishes all three before dp[i][j] needs them.

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
  1. 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.
  2. Row 0 is all 1s. Along the top edge the robot can only keep moving right, so each cell there has exactly one path.
  3. dp[1][0] = 1. The left column is an edge too: the only way down it is straight down.
  4. 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.
  5. 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.
  6. dp[1][3] = up + left = 1 + 3 = 4. 1 path arrives from above and 3 paths arrive from the left.
  7. dp[1][4] = up + left = 1 + 4 = 5. 1 path arrives from above and 4 paths arrive from the left.
  8. dp[1][5] = up + left = 1 + 5 = 6. 1 path arrives from above and 5 paths arrive from the left.
  9. dp[1][6] = up + left = 1 + 6 = 7. 1 path arrives from above and 6 paths arrive from the left.
  10. 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.
  11. dp[2][1] = up + left = 2 + 1 = 3. 2 paths arrive from above and 1 path arrives from the left.
  12. dp[2][2] = up + left = 3 + 3 = 6. 3 paths arrive from above and 3 paths arrive from the left.
  13. dp[2][3] = up + left = 4 + 6 = 10. 4 paths arrive from above and 6 paths arrive from the left.
  14. dp[2][4] = up + left = 5 + 10 = 15. 5 paths arrive from above and 10 paths arrive from the left.
  15. dp[2][5] = up + left = 6 + 15 = 21. 6 paths arrive from above and 15 paths arrive from the left.
  16. dp[2][6] = up + left = 7 + 21 = 28. 7 paths arrive from above and 21 paths arrive from the left.
  17. The corner holds the answer: 28 paths. Every one of the 21 cells 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] combines up and left: 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 (up or left) or, in Edit Distance, replaces one (diagonal). Longest Common Subsequence and Edit Distance.
  • Intervals. dp[i][j] covers the substring from i to j and 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 i to j, 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

OperationTimeSpace
Grid DP over m x n cellsO(m · n)O(m · n), or O(n) with one row
Two strings of lengths m and nO(m · n)O(m · n), or O(min(m, n)) with two rows
Interval DP with O(1) work per intervalO(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]
m = 3 rows, n = 7 columns. Every cell adds the one above it and the one to its left, and the corner holds the answer, 28.

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)); // 1
package 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.

Longest Common Subsequence

MEDIUMLeetCode 1143

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 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
Rows are prefixes of abcde and columns are prefixes of ace. The corner is the answer.

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
  1. 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.
  2. Row 0 and column 0 stand for an empty prefix. Nothing is common with "", so they stay 0.
  3. a == a, so this pair extends the LCS of the shorter prefixes "" and "", on the diagonal: dp[1][1] = 0 + 1 = 1.
  4. a != c, so at least one isn't in the LCS. Dropping a leaves 0 (up) and dropping c leaves 1 (left), so dp[1][2] = 1.
  5. a != e, so at least one isn't in the LCS. Dropping a leaves 0 (up) and dropping e leaves 1 (left), so dp[1][3] = 1.
  6. b != a, so at least one isn't in the LCS. Dropping b leaves 1 (up) and dropping a leaves 0 (left), so dp[2][1] = 1.
  7. b != c, so at least one isn't in the LCS. Dropping b leaves 1 (up) and dropping c leaves 1 (left), so dp[2][2] = 1.
  8. b != e, so at least one isn't in the LCS. Dropping b leaves 1 (up) and dropping e leaves 1 (left), so dp[2][3] = 1.
  9. c != a, so at least one isn't in the LCS. Dropping c leaves 1 (up) and dropping a leaves 0 (left), so dp[3][1] = 1.
  10. c == c, so this pair extends the LCS of the shorter prefixes "ab" and "a", on the diagonal: dp[3][2] = 1 + 1 = 2.
  11. c != e, so at least one isn't in the LCS. Dropping c leaves 1 (up) and dropping e leaves 2 (left), so dp[3][3] = 2.
  12. d != a, so at least one isn't in the LCS. Dropping d leaves 1 (up) and dropping a leaves 0 (left), so dp[4][1] = 1.
  13. d != c, so at least one isn't in the LCS. Dropping d leaves 2 (up) and dropping c leaves 1 (left), so dp[4][2] = 2.
  14. d != e, so at least one isn't in the LCS. Dropping d leaves 2 (up) and dropping e leaves 2 (left), so dp[4][3] = 2.
  15. e != a, so at least one isn't in the LCS. Dropping e leaves 1 (up) and dropping a leaves 0 (left), so dp[5][1] = 1.
  16. e != c, so at least one isn't in the LCS. Dropping e leaves 2 (up) and dropping c leaves 1 (left), so dp[5][2] = 2.
  17. e == e, so this pair extends the LCS of the shorter prefixes "abcd" and "ac", on the diagonal: dp[5][3] = 2 + 1 = 3.
  18. The corner holds the answer: "abcde" and "ace" share a subsequence of length 3, 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")); // 0
package 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
    }
}

Edit Distance

MEDIUMLeetCode 72

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 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] |                   |
 +-------------------+-------------------+
On a mismatch, dp[i][j] is 1 plus the smallest of the other three cells.
  • Replace word1[i - 1] with word2[j - 1]. Both last characters are now settled, leaving dp[i - 1][j - 1].
  • Delete word1[i - 1]. The remaining i - 1 characters still have to become all j, leaving dp[i - 1][j].
  • Insert word2[j - 1] at the end. That settles the last target character, so all i characters have to become the first j - 1, leaving dp[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
Rows are prefixes of horse and columns are prefixes of ros. The bracketed cells are the three that (3) reads.

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")); // 3
package 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] compares text1[i - 1] and text2[j - 1], not text1[i].
  • Copying base cases from a different problem. LCS starts its first row and column at 0, but Edit Distance starts them at i and j.
  • 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.