Lesson 184 min read

Dynamic Programming II — 2-D

November 05, 2026

// TL;DR

When a problem compares TWO sequences or walks a GRID, the state usually needs two indices → a 2-D table dp[i][j]. Each cell is built from its neighbours (up, left, diagonal). Unique paths: dp[i][j] = dp[i-1][j] + dp[i][j-1]. Longest Common Subsequence: if chars match dp[i][j] = dp[i-1][j-1] + 1 (diagonal), else max(dp[i-1][j], dp[i][j-1]). Edit distance: 1 + min(insert, delete, replace). 0/1 knapsack: take or skip each item. Fill row by row; the answer is usually the last cell. Many 2-D DPs compress to 1-D (you only need the previous row). Signals: 'two strings/arrays', 'grid paths', 'subsequence', 'min edits/operations', 'can you partition'. This closes the core patterns — Part II then covers tries, bit manipulation, greedy, and the advanced material.

The final pattern. When a problem involves two sequences or a grid, one index isn’t enough — you need a 2-D table. The idea is the same as 1-D DP (build each state from smaller ones); there’s just one more dimension.

The idea

Dynamic programming two, fill a 2-D table. An LCS grid comparing abcde across the top and ace down the side, filled with values; matching characters take the diagonal plus 1, non-matching take the max of up and left, and the answer is the bottom-right cell equal to 3, the LCS being ace. The idea: when a problem compares two sequences or walks a grid, the state usually needs two indices, a 2-D table dp i j; each cell is built from its neighbours; for Longest Common Subsequence if the characters match dp i j equals dp i minus 1 j minus 1 plus 1, the diagonal, if not dp i j equals max of dp i minus 1 j and dp i j minus 1, best of up and left; fill row by row and the answer is the last cell; the same shape solves edit distance, unique paths, and knapsack.

Define dp[i][j] over two indices — usually “the answer considering the first i of one thing and the first j of another.” Each cell is built from its neighbours (the cell above, to the left, and/or diagonally up-left). Fill the table row by row; the answer is usually the bottom-right cell.

Worked example: unique paths (grid)

How many paths from top-left to bottom-right of an m×n grid, moving only right or down? Each cell is reached from above or from the left: dp[i][j] = dp[i-1][j] + dp[i][j-1]. (Compressed to 1-D since we only need the previous row.)

def unique_paths(m, n):
    dp = [1] * n                     # first row is all 1s
    for _ in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]         # from above (dp[j]) + from left (dp[j-1])
    return dp[-1]
# unique_paths(3, 7) -> 28

Worked example: longest common subsequence

The canonical two-sequence DP. dp[i][j] = LCS length of the first i chars of A and first j of B. If the characters match, extend the diagonal; else take the best of dropping one character from either string:

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          # match → diagonal + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])  # best of up / left
    return dp[-1][-1]
# lcs("abcde", "ace") -> 3  ("ace")

Worked example: edit distance

Minimum insert/delete/replace operations to turn A into B. If chars match, no cost (diagonal); else 1 + min(insert, delete, replace):

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          # delete all
    for j in range(len(b)+1): dp[0][j] = j          # insert all
    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[-1][-1]
# edit_distance("horse", "ros") -> 3

Worked example: 0/1 knapsack

For each item, take it (gain its value, use its weight) or skip it — the archetype of “take or not take” DP. The 1-D compressed version iterates capacity backwards so each item is used once:

def knapsack(weights, values, W):
    dp = [0] * (W + 1)
    for i in range(len(weights)):
        for c in range(W, weights[i] - 1, -1):      # backwards → 0/1 (use each item once)
            dp[c] = max(dp[c], dp[c - weights[i]] + values[i])
    return dp[W]
# knapsack([1,3,4,5], [1,4,5,7], 7) -> 9

Knapsack-shaped DP (take/skip with a budget) underlies partition-equal-subset-sum, target-sum, and many “can you make exactly X” problems.

Step-by-step walkthrough

Here is the full LCS table for A = "abcde", B = "ace". Cell [i][j] is the LCS length of the first i letters of A and the first j letters of B. When the letters match, take the diagonal + 1; otherwise take the larger of up and left.

∅ 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

Fill it row by row; the answer is the bottom-right cell, 3 ("ace"). Notice each row only reads the row above — that’s why many 2-D DPs compress to a single 1-D array.

When to reach for it (and the 1-D compression)

Signals: two strings/arrays compared (LCS, edit distance, interleaving), grid paths (unique paths, min path sum, maximal square), subsequence problems, “min edits/operations”, or “can you partition/select to hit a target” (knapsack family). Many 2-D DPs compress to 1-D because each row only depends on the previous one — a nice optimisation to mention (O(n) space instead of O(n·m)).

Pitfalls

  • Index/base-case errors. 2-D tables have whole base rows and columns (e.g. edit distance’s dp[i][0]=i). Initialise them carefully.
  • Compressing too eagerly. The 1-D rolling optimisation is great, but get the full 2-D version correct first, then compress — and iterate in the right direction (knapsack goes backwards).
  • Wrong neighbour. Match → diagonal; mismatch → up/left (or up/left/diagonal for edit distance). Draw a few cells by hand to confirm the recurrence.

Recap

2-D DP handles two sequences or a grid with a table dp[i][j], each cell built from its neighbours: unique paths (up + left), LCS (match → diagonal+1, else max of up/left), edit distance (1 + min of three), knapsack (take/skip with a budget). Fill row by row; the answer is the last cell; often compressible to 1-D. Signal: two strings/arrays, grid paths, subsequence, min-operations, partition-to-target.

Practice: 12 solved problems on 2-D dynamic programming → — cover each one, solve it yourself, then check the tested code.

You’ve finished the core 18 patterns

That’s the main toolkit — the patterns behind the large majority of coding-interview questions. For each one, learn the template by heart, work the practice set, and in the interview name the pattern out loud before you code. Recognition is the whole skill, and it only comes from deliberate practice.

Part II — Extended & Advanced Patterns

The core 18 get you through most rounds. The next lessons round out the toolkit for the harder end of the MAANG bar — a data-structures reference, the patterns that come up often but aren’t in the core set (tries, bit manipulation, greedy), and the genuinely advanced material senior/staff interviews probe (weighted-graph algorithms, advanced DP, string matching).

Next: Lesson 19 — Data Structures: Your Toolbox.

exercisePractice this patternDynamic Programming II (2-D) — Practice Problemsarrow_forward