Dynamic Programming (2D)
Extend DP to two-dimensional state spaces: grid path counting, two-string comparison (LCS, edit distance), and 2D knapsack variants. Master the dp[i][j] table, understand how to set up base cases for rows and columns, and optimize space with rolling arrays.
Learn & Reference
Understanding the Pattern
Dynamic Programming (2D) Pattern
2D Dynamic Programming extends the 1D concepts you already know to problems with two-dimensional state. Instead of dp[i], you now fill a 2D table dp[i][j] where each cell depends on its neighbors.
Grid DP (Unique Paths): Two-String DP (LCS):
1 1 1 1 "" a c e
1 2 3 4 "" 0 0 0 0
1 3 6 10 a 0 1 1 1
b 0 1 1 1
dp[i][j] = dp[i-1][j] c 0 1 2 2
+ dp[i][j-1] d 0 1 2 2
e 0 1 2 3
Three Major Archetypes
1. Grid DP
The state is a position in a grid. You can typically only move right or down (or from top-left neighbors).
dp[i][j] = f(dp[i-1][j], dp[i][j-1])
Examples: Unique Paths, Minimum Path Sum, Dungeon Game
Base cases: First row and first column are filled directly (only one way to reach them).
2. Two-String DP
The state represents prefixes of two strings: dp[i][j] = answer for s1[0..i) and s2[0..j).
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # characters match
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip one character
Examples: Longest Common Subsequence, Edit Distance, Wildcard Matching
Base cases: dp[0][j] and dp[i][0] represent comparing with an empty string.
3. 2D Knapsack
One dimension is the item index, the other is the capacity/target. Often space-optimized to 1D.
Full 2D: dp[i][j] = dp[i-1][j] or dp[i-1][j - nums[i]]
1D opt: dp[j] += dp[j - coin] (iterate right for unbounded)
dp[j] += dp[j - num] (iterate left for 0/1)
Examples: Coin Change II, Target Sum, 0/1 Knapsack, Subset Sum
Base case: dp[0] = 1 (one way to make amount 0: use nothing).
Space Optimization
When dp[i][j] only depends on the current row and the row above, you can use a rolling array (two 1D arrays) or even a single 1D array updated carefully:
# Grid DP: single row, update left-to-right
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
# 0/1 Knapsack: single row, update RIGHT-TO-LEFT (preserve previous row values)
for num in nums:
for j in range(target, num - 1, -1):
dp[j] += dp[j - num]
# Unbounded Knapsack: single row, update LEFT-TO-RIGHT (allow reuse)
for coin in coins:
for j in range(coin, amount + 1):
dp[j] += dp[j - coin]
DFS + Memoization on Grids
Some 2D DP problems are easier solved top-down with DFS + memoization rather than bottom-up tabulation. This is especially true for grid problems where the dependency direction varies (e.g., longest increasing path can go in any of 4 directions).
memo = {}
def dfs(i, j):
if (i, j) in memo: return memo[(i, j)]
result = 1 # at least the cell itself
for di, dj in [(0,1),(0,-1),(1,0),(-1,0)]:
ni, nj = i+di, j+dj
if in_bounds(ni, nj) and grid[ni][nj] > grid[i][j]:
result = max(result, 1 + dfs(ni, nj))
memo[(i, j)] = result
return result
Common Mistakes
- Wrong table dimensions — For two-string DP, the table should be
(m+1) x (n+1)to include the empty string. Usingm x ncauses off-by-one errors and missing base cases. - Forgetting base case initialization — The first row and first column of the DP table need explicit initialization. For grid DP, the first row/column is often all 1s. For edit distance,
dp[i][0] = ianddp[0][j] = j. - String indexing vs DP indexing — When
dp[i][j]represents prefixes of lengthiandj, the corresponding string characters ares1[i-1]ands2[j-1], nots1[i]ands2[j]. This off-by-one is the #1 bug in two-string DP. - Wrong iteration direction for knapsack — For 0/1 knapsack (each item used once), iterate capacity right-to-left. For unbounded knapsack (items reusable), iterate left-to-right. Reversing these produces wrong answers.
- Not handling edge cases — Empty strings, single-cell grids, zero target amounts, and impossible cases (odd target sum) all need special handling before the main DP loop.
When to Use
Template
1# --- Grid DP Template ---2def solve_grid(grid):3m, n = len(grid), len(grid[0])4dp = [[0] * n for _ in range(m)]56# Base cases: first row and first column7dp[0][0] = grid[0][0]8for j in range(1, n): dp[0][j] = f(dp[0][j-1], grid[0][j])9for i in range(1, m): dp[i][0] = f(dp[i-1][0], grid[i][0])1011# Fill table12for i in range(1, m):13for j in range(1, n):14dp[i][j] = f(dp[i-1][j], dp[i][j-1], grid[i][j])1516return dp[m-1][n-1]1718# --- Two-String DP Template ---19def solve_strings(s1, s2):20m, n = len(s1), len(s2)21dp = [[0] * (n + 1) for _ in range(m + 1)]2223# Base cases: dp[i][0] and dp[0][j]2425for i in range(1, m + 1):26for j in range(1, n + 1):27if s1[i-1] == s2[j-1]:28dp[i][j] = dp[i-1][j-1] + 1 # match29else:30dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # no match3132return dp[m][n]
Syntax Reference
1# --- 2D Array Initialization ---2dp = [[0] * (n + 1) for _ in range(m + 1)] # (m+1) x (n+1) table3dp = [[float('inf')] * (n + 1) for _ in range(m + 1)] # For minimization4dp = [[False] * (n + 1) for _ in range(m + 1)] # For boolean DP56# WARNING: dp = [[0] * n] * m ← WRONG! All rows share same list78# --- Nested Iteration ---9for i in range(1, m + 1):10for j in range(1, n + 1):11dp[i][j] = f(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])1213# --- Space Optimization (rolling 1D array) ---14dp = [0] * (n + 1)15for i in range(1, m + 1):16prev = dp[0] # save dp[i-1][j-1]17for j in range(1, n + 1):18temp = dp[j]19dp[j] = f(dp[j], dp[j-1], prev) # dp[j] = above, dp[j-1] = left20prev = temp2122# --- DFS + Memoization (grid) ---23from functools import lru_cache2425@lru_cache(maxsize=None)26def dfs(i, j):27# base case + recurse on neighbors28pass
Common Recipes
1# Recipe 1: Unique Paths (Grid DP, space-optimized)2def unique_paths(m, n):3dp = [1] * n4for i in range(1, m):5for j in range(1, n):6dp[j] += dp[j - 1]7return dp[n - 1]89# Recipe 2: Longest Common Subsequence (Two-String DP)10def longest_common_subsequence(text1, text2):11m, n = len(text1), len(text2)12dp = [[0] * (n + 1) for _ in range(m + 1)]13for i in range(1, m + 1):14for j in range(1, n + 1):15if text1[i - 1] == text2[j - 1]:16dp[i][j] = dp[i - 1][j - 1] + 117else:18dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])19return dp[m][n]2021# Recipe 3: Edit Distance (Two-String DP)22def min_distance(word1, word2):23m, n = len(word1), len(word2)24dp = [[0] * (n + 1) for _ in range(m + 1)]25for i in range(m + 1): dp[i][0] = i26for j in range(n + 1): dp[0][j] = j27for i in range(1, m + 1):28for j in range(1, n + 1):29if word1[i - 1] == word2[j - 1]:30dp[i][j] = dp[i - 1][j - 1]31else:32dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])33return dp[m][n]3435# Recipe 4: 0/1 Knapsack Counting (space-optimized)36def knapsack_count(nums, target):37dp = [0] * (target + 1)38dp[0] = 139for num in nums:40for j in range(target, num - 1, -1): # RIGHT-TO-LEFT for 0/141dp[j] += dp[j - num]42return dp[target]
Complexity
| Archetype | Time | Space | Example |
|---|---|---|---|
| Grid DP | O(m · n) | O(n)* | Unique Paths, Min Path Sum |
| Two-String DP | O(m · n) | O(m · n) | LCS, Edit Distance |
| 2D Knapsack | O(n · W) | O(W)* | Coin Change II, Target Sum |
| Grid DFS + Memo | O(m · n) | O(m · n) | Longest Increasing Path |
Watch Out
- ✗Wrong table dimensions — For two-string DP, the table should be `(m+1) x (n+1)` to include the empty string. Using `m x n` causes off-by-one errors and missing base cases.
- ✗Forgetting base case initialization — The first row and first column of the DP table need explicit initialization. For grid DP, the first row/column is often all 1s. For edit distance, `dp[i][0] = i` and `dp[0][j] = j`.
- ✗String indexing vs DP indexing — When `dp[i][j]` represents prefixes of length `i` and `j`, the corresponding string characters are `s1[i-1]` and `s2[j-1]`, not `s1[i]` and `s2[j]`. This off-by-one is the #1 bug in two-string DP.
- ✗Wrong iteration direction for knapsack — For 0/1 knapsack (each item used once), iterate capacity right-to-left. For unbounded knapsack (items reusable), iterate left-to-right. Reversing these produces wrong answers.
- ✗Not handling edge cases — Empty strings, single-cell grids, zero target amounts, and impossible cases (odd target sum) all need special handling before the main DP loop.