DSA Tutorial
🔍

2D Dynamic Programming

When Does a Problem Need 2D DP?

A problem needs 2D DP when the state cannot be captured by a single index. Two indices are required.

SIGNALS THAT YOU NEED 2D DP:
  "Two strings or sequences" → dp[i][j] = answer for s1[0..i-1] and s2[0..j-1]
  "Grid movement"            → dp[i][j] = answer to reach cell (i,j)
  "Interval [i..j]"         → dp[i][j] = answer for the sub-array from i to j
  "Item i with remaining w"  → dp[i][w] = answer using i items with capacity w

2D STATE SHAPES:
  dp[i][j] where i,j are both POSITIONS in same array (interval DP)
  dp[i][j] where i is position in s1, j in s2 (two-string DP)
  dp[i][j] where i is row, j is column (grid DP)
  dp[i][w] where i is item index, w is capacity (knapsack-style)

FILL ORDERS BY SHAPE:
  Grid DP:       Row by row, left to right (i outer, j inner)
  Two-string DP: Row by row, left to right (same)
  Interval DP:   By increasing interval LENGTH (non-obvious)

Pattern 1: Grid DP

Unique Paths

STATE:   dp[i][j] = number of paths to reach cell (i,j)
RECUR:   dp[i][j] = dp[i-1][j] + dp[i][j-1]  (from above or from left)
BASE:    dp[0][j] = 1 for all j  (first row: only rightward moves)
         dp[i][0] = 1 for all i  (first col: only downward moves)
FILL:    Row by row, left to right
ANSWER:  dp[m-1][n-1]

Table for 3×4 grid:
      j=0  j=1  j=2  j=3
i=0 [  1,   1,   1,   1 ]
i=1 [  1,   2,   3,   4 ]
i=2 [  1,   3,   6,  10 ]

dp[1][1] = dp[0][1]+dp[1][0] = 1+1 = 2
dp[2][3] = dp[1][3]+dp[2][2] = 4+6 = 10

Minimum Path Sum

STATE:   dp[i][j] = min cost to reach cell (i,j)
RECUR:   dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
BASE:    dp[0][0] = grid[0][0]
         dp[0][j] = dp[0][j-1] + grid[0][j]  (first row: only right)
         dp[i][0] = dp[i-1][0] + grid[i][0]  (first col: only down)
FILL:    Row by row, left to right
ANSWER:  dp[m-1][n-1]

Grid:             dp table:
  1  3  1           1  4  5
  1  5  1    →      2  7  6
  4  2  1           6  8  7

dp[1][1]=grid[1][1]+min(dp[0][1],dp[1][0])=5+min(4,2)=7
dp[2][3]=grid[2][2]+min(dp[1][2],dp[2][1])=1+min(6,8)=7
Answer = 7 (path: 1→3→1→1→1 = 7, but 1→1→5→1→... no)
         Check: 1→1→1→1→1 = path 1→right? No.
         Path: (0,0)→(1,0)→(2,0)→(2,1)→(2,2) = 1+1+4+2+1=9? No.
         Path: (0,0)→(0,1)→(0,2)→(1,2)→(2,2) = 1+3+1+1+1=7 ✓

Unique Paths II (With Obstacles)

Same as Unique Paths but obstacle cells = 0 paths (dp[i][j] = 0).

RECUR:   if grid[i][j] == 1 (obstacle): dp[i][j] = 0
         else: dp[i][j] = dp[i-1][j] + dp[i][j-1]

KEY: Obstacles block all paths through that cell.
An obstacle in first row/col zeros out ALL subsequent cells in that row/col.

Pattern 2: Two-String DP

Longest Common Subsequence (LCS)

STATE:   dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]
RECUR:   if s1[i-1]==s2[j-1]: dp[i][j] = 1 + dp[i-1][j-1]  (match: extend)
         else:                 dp[i][j] = max(dp[i-1][j],    (skip s1[i-1])
                                              dp[i][j-1])    (skip s2[j-1])
BASE:    dp[0][j] = 0 for all j  (empty s1: LCS = 0)
         dp[i][0] = 0 for all i  (empty s2: LCS = 0)
FILL:    Row by row, left to right
ANSWER:  dp[m][n]

Table for s1="abcde", s2="ace":

      ""  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]

LCS = 3 (subsequence "ace") ✓

Edit Distance (Levenshtein Distance)

STATE:   dp[i][j] = min edits to transform s1[0..i-1] into s2[0..j-1]
         Operations: insert, delete, replace — each costs 1

RECUR:   if s1[i-1]==s2[j-1]:  dp[i][j] = dp[i-1][j-1]     (no cost: match)
         else:                  dp[i][j] = 1 + min(
           dp[i-1][j],                                       (delete s1[i-1])
           dp[i][j-1],                                       (insert s2[j-1])
           dp[i-1][j-1])                                     (replace)

BASE:    dp[0][j] = j  (transform "" into s2[0..j-1]: j inserts)
         dp[i][0] = i  (transform s1[0..i-1] into "": i deletes)
FILL:    Row by row, left to right
ANSWER:  dp[m][n]

Table for s1="horse", s2="ros":

      ""  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]

Edit distance = 3 (horse→rorse→rose→ros) ✓

Pattern 3: Interval DP

Interval DP answers questions about sub-arrays or sub-strings of form [i..j].

KEY INSIGHT:
  dp[i][j] = answer for the interval [i, j]
  Depends on SHORTER intervals: dp[i][k] and dp[k+1][j] for some split k

FILL ORDER — by increasing interval length:
  for length in 2..n:             ← interval length
    for i in 0..n-length:         ← start of interval
      j = i + length - 1          ← end of interval
      for k in i..j-1:            ← split point
        dp[i][j] = combine(dp[i][k], dp[k+1][j])

BASE: dp[i][i] = answer for single element (length 1)

Burst Balloons

PROBLEM: Burst all balloons, earn nums[left]*nums[i]*nums[right] when bursting i.
         Maximise total coins.

TRICK: Think BACKWARDS — which balloon is burst LAST in interval [i,j]?

STATE:   dp[i][j] = max coins from bursting ALL balloons in (i,j)
         (i and j are kept as boundaries, not burst)
RECUR:   dp[i][j] = max over k in (i,j):
           nums[i] * nums[k] * nums[j] + dp[i][k] + dp[k][j]
         k = the LAST balloon burst in (i,j)

BASE:    dp[i][j] = 0 when interval is empty (j = i+1)
         Pad array: nums = [1] + nums + [1] (virtual boundary balloons)

For nums = [3,1,5,8], padded = [1,3,1,5,8,1]:

Interval lengths 2,3,4,5,6 filled in order.
dp[0][5] = max total coins = 167 ✓

Matrix Chain Multiplication

PROBLEM: Given matrix dimensions, find optimal parenthesisation
         to minimise total multiplications.

STATE:   dp[i][j] = min multiplications to compute product A_i * ... * A_j
RECUR:   dp[i][j] = min over k in [i,j-1]:
           dp[i][k] + dp[k+1][j] + dims[i-1]*dims[k]*dims[j]
           (cost of left chain) + (cost of right chain) + (cost of final multiply)

BASE:    dp[i][i] = 0  (single matrix: no multiplications)
FILL:    By increasing interval length
ANSWER:  dp[1][n]  (chain of n matrices)

dims = [10, 30, 5, 60]:
  Matrices: A1(10×30), A2(30×5), A3(5×60)

  dp[1][1]=0, dp[2][2]=0, dp[3][3]=0  (base)

  Length 2:
    dp[1][2]: k=1: 0+0+10*30*5=1500. dp[1][2]=1500
    dp[2][3]: k=2: 0+0+30*5*60=9000. dp[2][3]=9000

  Length 3:
    dp[1][3]:
      k=1: dp[1][1]+dp[2][3]+10*30*60 = 0+9000+18000 = 27000
      k=2: dp[1][2]+dp[3][3]+10*5*60  = 1500+0+3000  = 4500 ← min
    dp[1][3] = 4500

Answer: 4500 (parenthesise as (A1*A2)*A3) ✓

Complete Implementations

1import java.util.*; 2 3public class TwoDimDP { 4 5 // ── Unique Paths ───────────────────────────────────────────────── 6 public static int uniquePaths(int m, int n) { 7 int[][] dp = new int[m][n]; 8 for (int i=0;i<m;i++) dp[i][0] = 1; 9 for (int j=0;j<n;j++) dp[0][j] = 1; 10 for (int i=1;i<m;i++) 11 for (int j=1;j<n;j++) 12 dp[i][j] = dp[i-1][j] + dp[i][j-1]; 13 return dp[m-1][n-1]; 14 } 15 16 // ── Minimum Path Sum ───────────────────────────────────────────── 17 public static int minPathSum(int[][] grid) { 18 int m=grid.length, n=grid[0].length; 19 int[][] dp = new int[m][n]; 20 dp[0][0] = grid[0][0]; 21 for (int j=1;j<n;j++) dp[0][j] = dp[0][j-1] + grid[0][j]; 22 for (int i=1;i<m;i++) dp[i][0] = dp[i-1][0] + grid[i][0]; 23 for (int i=1;i<m;i++) 24 for (int j=1;j<n;j++) 25 dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]); 26 return dp[m-1][n-1]; 27 } 28 29 // ── LCS ────────────────────────────────────────────────────────── 30 public static int lcs(String s1, String s2) { 31 int m=s1.length(), n=s2.length(); 32 int[][] dp = new int[m+1][n+1]; // dp[0][*] and dp[*][0] = 0 by default 33 for (int i=1;i<=m;i++) 34 for (int j=1;j<=n;j++) 35 dp[i][j] = s1.charAt(i-1)==s2.charAt(j-1) 36 ? 1 + dp[i-1][j-1] 37 : Math.max(dp[i-1][j], dp[i][j-1]); 38 return dp[m][n]; 39 } 40 41 // ── Edit Distance ──────────────────────────────────────────────── 42 public static int editDistance(String s1, String s2) { 43 int m=s1.length(), n=s2.length(); 44 int[][] dp = new int[m+1][n+1]; 45 46 // Base cases: transform to/from empty string 47 for (int i=0;i<=m;i++) dp[i][0] = i; // i deletes 48 for (int j=0;j<=n;j++) dp[0][j] = j; // j inserts 49 50 for (int i=1;i<=m;i++) 51 for (int j=1;j<=n;j++) { 52 if (s1.charAt(i-1)==s2.charAt(j-1)) 53 dp[i][j] = dp[i-1][j-1]; // Match: no cost 54 else 55 dp[i][j] = 1 + Math.min(dp[i-1][j], 56 Math.min(dp[i][j-1], dp[i-1][j-1])); 57 } 58 return dp[m][n]; 59 } 60 61 // ── Burst Balloons ─────────────────────────────────────────────── 62 public static int maxCoins(int[] nums) { 63 int n = nums.length; 64 // Pad with virtual boundary balloons 65 int[] padded = new int[n + 2]; 66 padded[0] = 1; padded[n+1] = 1; 67 for (int i=0;i<n;i++) padded[i+1] = nums[i]; 68 69 int N = padded.length; 70 int[][] dp = new int[N][N]; 71 72 // Fill by interval length (length 2 to N-1) 73 for (int len=2;len<N;len++) { 74 for (int i=0;i<=N-len-1;i++) { 75 int j = i + len; 76 for (int k=i+1;k<j;k++) { 77 int coins = padded[i]*padded[k]*padded[j] 78 + dp[i][k] + dp[k][j]; 79 dp[i][j] = Math.max(dp[i][j], coins); 80 } 81 } 82 } 83 return dp[0][N-1]; 84 } 85 86 // ── Matrix Chain Multiplication ────────────────────────────────── 87 public static int matrixChain(int[] dims) { 88 int n = dims.length - 1; // Number of matrices 89 int[][] dp = new int[n+1][n+1]; 90 91 // Fill by increasing chain length 92 for (int len=2;len<=n;len++) { 93 for (int i=1;i<=n-len+1;i++) { 94 int j = i + len - 1; 95 dp[i][j] = Integer.MAX_VALUE; 96 for (int k=i;k<j;k++) { 97 int cost = dp[i][k] + dp[k+1][j] 98 + dims[i-1]*dims[k]*dims[j]; 99 dp[i][j] = Math.min(dp[i][j], cost); 100 } 101 } 102 } 103 return dp[1][n]; 104 } 105 106 public static void main(String[] args) { 107 System.out.println("Unique Paths(3,4): " + uniquePaths(3,4)); // 10 108 System.out.println("Min Path Sum: " + 109 minPathSum(new int[][]{{1,3,1},{1,5,1},{4,2,1}})); // 7 110 System.out.println("LCS(abcde,ace): " + lcs("abcde","ace")); // 3 111 System.out.println("Edit(horse,ros): " + editDistance("horse","ros")); // 3 112 System.out.println("Burst [3,1,5,8]: " + maxCoins(new int[]{3,1,5,8})); // 167 113 System.out.println("Matrix Chain: " + matrixChain(new int[]{10,30,5,60})); // 4500 114 } 115}
Output:
Unique Paths(3,4):   10
Min Path Sum:         7
LCS(abcde,ace):       3
Edit Distance(horse,ros): 3
Burst Balloons:     167
Matrix Chain:       4500

Space Optimisation for 2D DP

Grid DP: O(m×n) → O(n)

For Unique Paths and Min Path Sum:
dp[i][j] only needs dp[i-1][j] (above) and dp[i][j-1] (left in same row).

Use single 1D array — it holds previous row initially, then gets updated left-to-right:

  prev = [1]*n   ← represents first row

  for i in 1..m-1:
    curr = [1] + [0]*(n-1)
    for j in 1..n-1:
      curr[j] = prev[j] + curr[j-1]
    prev = curr

  return prev[n-1]

Unique Paths: O(m×n) → O(n)
Min Path Sum: O(m×n) → O(n)

Two-String DP: O(m×n) → O(n)

For LCS and Edit Distance:
dp[i][j] needs dp[i-1][j-1], dp[i-1][j], dp[i][j-1].
Only the previous row is needed.

LCS with rolling row:
  dp = [0]*(n+1)
  for i in 1..m:
    new_dp = [0]*(n+1)
    for j in 1..n:
      if s1[i-1]==s2[j-1]: new_dp[j] = 1 + dp[j-1]  ← diagonal: dp[i-1][j-1]
      else: new_dp[j] = max(dp[j], new_dp[j-1])       ← dp[i-1][j] and dp[i][j-1]
    dp = new_dp
  return dp[n]

LCS: O(m×n) → O(n)
Edit Distance: O(m×n) → O(n) (same rolling row pattern)

Interval DP Fill Order Visualised

Interval DP fills by LENGTH, not by row:

For array of size 4: indices 0,1,2,3

Step 1 — Length 1 (base cases): fill diagonal
  dp[0][0]=0, dp[1][1]=0, dp[2][2]=0, dp[3][3]=0

Step 2 — Length 2: adjacent pairs
  dp[0][1], dp[1][2], dp[2][3]

Step 3 — Length 3:
  dp[0][2], dp[1][3]

Step 4 — Length 4 (answer):
  dp[0][3]

TABLE FILLED IN ORDER:
      j=0  j=1  j=2  j=3
i=0 [ ①,   ②,   ③,   ④ ]
i=1 [  _,   ①,   ②,   ③ ]
i=2 [  _,    _,   ①,   ② ]
i=3 [  _,    _,    _,   ① ]

Fill diagonal first, then upper-right triangle by increasing offset.
Only the upper triangle (i ≤ j) is filled (intervals [i,j] only valid when i≤j).

Pattern Summary

PATTERN              STATE           FILL ORDER          EXAMPLE
─────────────────────────────────────────────────────────────────────
Grid movement        dp[i][j]        Row by row          Unique Paths
                     = answer        (i outer, j inner)  Min Path Sum
                     to reach (i,j)

Two strings          dp[i][j]        Row by row          LCS
                     = answer for    (i outer, j inner)  Edit Distance
                     s1[:i], s2[:j]                      Longest Common
                                                          Substring

Interval             dp[i][j]        By interval length  Burst Balloons
                     = answer for    (len outer, i inner, Matrix Chain
                     subarray [i,j]  j = i+len-1)        Palindrome Partition

Item + capacity      dp[i][w]        Row by row          0/1 Knapsack
                     = answer using  (i outer, w inner)  Unbounded Knapsack
                     items[0..i-1]
                     with capacity w

Complexity Summary

ProblemTimeSpaceOptimised Space
Unique PathsO(m×n)O(m×n)O(n)
Min Path SumO(m×n)O(m×n)O(n)
LCSO(m×n)O(m×n)O(n)
Edit DistanceO(m×n)O(m×n)O(n)
Burst BalloonsO(n³)O(n²)Cannot reduce
Matrix ChainO(n³)O(n²)Cannot reduce

Common Mistakes

Edit Distance: base case dp[i][0] = i, not dp[i][0] = 0. dp[i][0] means "transform s1[0..i-1] into an empty string" — that requires i deletions. dp[0][j] = j means "transform empty string into s2[0..j-1]" — j insertions. Initialising these to 0 breaks the recurrence for the first row and column.

Interval DP: filling by row instead of by interval length. If you fill dp[1][3] before dp[2][3], the recurrence dp[1][3] = min over k of dp[1][k]+dp[k+1][3] references dp[2][3] which is still 0 — wrong. Always fill shorter intervals before longer ones.

Burst Balloons: not padding with boundary 1s. When balloon k is the last burst in interval (i,j), it earns nums[i]*nums[k]*nums[j] — but i and j are boundaries, not burst in this interval. Padding with [1]+nums+[1] provides the virtual boundaries without special-casing.

LCS vs Longest Common Substring confusion. LCS (subsequence) allows skipping characters — dp[i][j] = max(dp[i-1][j], dp[i][j-1]) when no match. Longest Common Substring (contiguous) requires: dp[i][j] = dp[i-1][j-1]+1 if match, else 0. The result is max(dp[i][j]) over all cells, not dp[m][n].

2D array allocation in Java: new int[m+1][n+1] vs new int[m][n]. For LCS and Edit Distance, use [m+1][n+1] to accommodate empty string base cases at index 0. For grid problems (Unique Paths), use [m][n] since the grid indices start at 0. Mixing these up causes index-out-of-bounds or wrong base case positions.

Interview Questions

Q: Edit Distance — what do the three recurrence cases (dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) each correspond to?

dp[i-1][j] + 1 = delete operation: remove s1[i-1], then solve the remaining problem of transforming s1[0..i-2] into s2[0..j-1]. dp[i][j-1] + 1 = insert operation: insert s2[j-1] at the end of s1[0..i-1], then the remaining problem is transforming s1[0..i-1] into s2[0..j-2]. dp[i-1][j-1] + 1 = replace operation: replace s1[i-1] with s2[j-1], then solve the remaining problem of transforming s1[0..i-2] into s2[0..j-2].

Q: Why is Burst Balloons solved by thinking about which balloon is burst LAST rather than first?

If we think about which balloon is burst first at position k in interval [i,j], the coins are nums[left]*nums[k]*nums[right] — but left and right depend on which adjacent balloons still exist, which changes as other balloons burst. This creates complicated dependencies. Thinking about the last balloon k burst in interval (i,j): since k is last, the sub-intervals (i,k) and (k,j) are fully burst first, and k sees boundaries i and j (which are not burst in this interval). The cost is a clean formula: nums[i]*nums[k]*nums[j] + dp[i][k] + dp[k][j].

Q: When can a 2D DP table be space-optimised to O(n), and when can it not?

Space reduction from O(m×n) to O(n) is possible when computing dp[i][j] only needs the current row (being built) and the previous row (already complete). This applies to grid DP (Unique Paths, Min Path Sum), two-string DP (LCS, Edit Distance), and row-by-row knapsack. It does NOT apply to interval DP (Burst Balloons, Matrix Chain) because dp[i][j] depends on dp[i][k] and dp[k+1][j] — references to multiple rows simultaneously, not just the adjacent previous row.

Summary

2D DP captures state using two indices: row+column for grids, position-in-each-string for two-string problems, or interval-start+interval-end for interval DP.

Four patterns:

PatternStateFill orderKey recurrence
Griddp[i][j] = answer at cellRow by rowdp[i-1][j] + dp[i][j-1]
Two stringsdp[i][j] = answer for s1[:i], s2[:j]Row by rowMatch: 1+dp[i-1][j-1], else max/min
Intervaldp[i][j] = answer for [i..j]By interval lengthmin/max over all split points k
Item+capacitydp[i][w] = best using items[:i] with wRow by rowmax(skip, take)

Space optimisation:

  • Grid / two-string: O(m×n) → O(n) using rolling row
  • Interval DP: cannot reduce below O(n²) — needs full upper triangle

The non-obvious insight for interval DP: fill by interval length, not by row. Dependencies are shorter intervals, not previous rows.

In the next topic you will explore Knapsack Pattern — the 0/1 knapsack, unbounded knapsack, and subset sum with all their variations.

Suggested Quiz

Unique Paths: dp[i][j] = dp[i-1][j] + dp[i][j-1]. The first row and column are all 1s. Why?

1/6