2D DP Patterns

advanced dynamic-programming 2D-DP edit-distance LCS

2D DP kicks in when our state needs two dimensions to describe. Instead of a single array dp[i], we use a matrix dp[i][j]. The value at each cell depends on its neighbors — usually the cell above, to the left, or diagonally.

In simple language, we’re filling a grid instead of a line. Same idea, one extra dimension.

Unique Paths

Problem: We’re at the top-left of an m x n grid. We can only move right or down. How many unique paths to the bottom-right corner?

State: dp[i][j] = number of paths to reach cell (i, j)

Recurrence: dp[i][j] = dp[i-1][j] + dp[i][j-1] (came from above or left)

Unique Paths DP Table (3x4 grid)
1 1 1 1
1 2 3 4
1 3 6 10
First row and column are all 1s (only one way to get there). Each cell = top + left.
function uniquePaths(m, n) {
  const dp = Array.from({ length: m }, () => new Array(n).fill(1));
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // top + left
    }
  }
  return dp[m - 1][n - 1];
}
// Time: O(m*n), Space: O(m*n) — can optimize to O(n)
def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]  # top + left
    return dp[m - 1][n - 1]
# Time: O(m*n), Space: O(m*n) — can optimize to O(n)
static int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int[] row : dp) Arrays.fill(row, 1);
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // top + left
        }
    }
    return dp[m - 1][n - 1];
}
// Time: O(m*n), Space: O(m*n) — can optimize to O(n)

Longest Common Subsequence (LCS)

Problem: Given two strings, find the length of their longest common subsequence. A subsequence doesn’t need to be contiguous — we can skip characters.

Example: "abcde" and "ace" have LCS of "ace" (length 3).

State: dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]

Recurrence:

  • If text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 (characters match, extend)
  • Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) (skip one or the other)
LCS Table: "abcde" vs "ace"
"" a b c d e
"" 0 0 0 0 0 0
a 0 1 1 1 1 1
c 0 1 1 2 2 2
e 0 1 1 2 2 3
Highlighted cells = character match (diagonal + 1). Answer is bottom-right: 3.
function longestCommonSubsequence(text1, text2) {
  const m = text1.length, n = text2.length;
  const dp = Array.from({ length: m + 1 }, () => new Array(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;        // match! extend diagonal
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // skip one
      }
    }
  }
  return dp[m][n];
}
// Time: O(m*n), Space: O(m*n)
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] = dp[i - 1][j - 1] + 1        # match! extend
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])  # skip one
    return dp[m][n]
# Time: O(m*n), Space: O(m*n)
static int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length(), n = text2.length();
    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;       // match! extend
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}
// Time: O(m*n), Space: O(m*n)

Edit Distance (Levenshtein Distance)

Problem: Given two strings, find the minimum number of operations (insert, delete, replace) to convert one into the other. This is a big interview favorite.

State: dp[i][j] = minimum edits to convert word1[0..i-1] into word2[0..j-1]

Recurrence:

  • If characters match: dp[i][j] = dp[i-1][j-1] (no edit needed)
  • Else: dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) (replace, delete, insert)
Where Each Operation Comes From
dp[i-1][j-1]
Replace
(diagonal)
dp[i-1][j]
Delete
(from above)
dp[i][j-1]
Insert
(from left)
function minDistance(word1, word2) {
  const m = word1.length, n = word2.length;
  const dp = Array.from({ length: m + 1 }, (_, i) =>
    new Array(n + 1).fill(0).map((_, j) => (i === 0 ? j : j === 0 ? i : 0))
  );
  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];              // chars match, no edit
      } else {
        dp[i][j] = 1 + Math.min(
          dp[i - 1][j - 1],  // replace
          dp[i - 1][j],      // delete
          dp[i][j - 1]       // insert
        );
      }
    }
  }
  return dp[m][n];
}
def min_distance(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(m + 1): dp[i][0] = i   # deleting all chars
    for j in range(n + 1): dp[0][j] = j   # inserting all 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]         # chars match
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j - 1],  # replace
                    dp[i - 1][j],      # delete
                    dp[i][j - 1]       # insert
                )
    return dp[m][n]
static int minDistance(String word1, String word2) {
    int m = word1.length(), n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i <= m; i++) dp[i][0] = i;  // delete all
    for (int j = 0; j <= n; j++) dp[0][j] = j;  // insert all
    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];
            } else {
                dp[i][j] = 1 + Math.min(dp[i - 1][j - 1],
                    Math.min(dp[i - 1][j], dp[i][j - 1]));
            }
        }
    }
    return dp[m][n];
}

Minimum Path Sum

Problem: Given a grid with non-negative numbers, find the path from top-left to bottom-right that minimizes the sum. We can only move right or down.

This is like unique paths but instead of counting paths, we’re minimizing cost.

State: dp[i][j] = minimum sum to reach cell (i, j)

function minPathSum(grid) {
  const m = grid.length, n = grid[0].length;
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (i === 0 && j === 0) continue;     // starting cell
      const top = i > 0 ? grid[i - 1][j] : Infinity;
      const left = j > 0 ? grid[i][j - 1] : Infinity;
      grid[i][j] += Math.min(top, left);     // add cheaper path
    }
  }
  return grid[m - 1][n - 1];
}
// We modified the input grid in-place — O(1) extra space!
def min_path_sum(grid):
    m, n = len(grid), len(grid[0])
    for i in range(m):
        for j in range(n):
            if i == 0 and j == 0: continue   # starting cell
            top = grid[i - 1][j] if i > 0 else float('inf')
            left = grid[i][j - 1] if j > 0 else float('inf')
            grid[i][j] += min(top, left)      # add cheaper path
    return grid[m - 1][n - 1]
# We modified the input grid in-place — O(1) extra space!
static int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (i == 0 && j == 0) continue;
            int top = i > 0 ? grid[i - 1][j] : Integer.MAX_VALUE;
            int left = j > 0 ? grid[i][j - 1] : Integer.MAX_VALUE;
            grid[i][j] += Math.min(top, left);
        }
    }
    return grid[m - 1][n - 1];
}
// We modified the input grid in-place — O(1) extra space!

The 2D DP Pattern

The pattern across all these problems:

  1. State needs two indices — two strings, grid row/col, two pointers into sequences
  2. Fill order is top-to-bottom, left-to-right — so dependencies are already computed
  3. Base cases fill the first row and first column (or the 0th row/column if we use padding)
  4. Answer is at dp[m][n] (or dp[m-1][n-1] depending on indexing)

Space optimization trick: If each row only depends on the previous row, we can use a 1D array and overwrite it as we go. This drops space from O(m*n) to O(n).