DSA Notes · chapter 04 · Java Live Code

Say the recurrence before you code

Ways add. Scores take max or min. The same suffix is asked more than once — that is the overlapping subproblem. Write the recurrence on paper, then memo, then a table, then rolling variables. Notebook Java for named families lives behind the tables, not in this chapter.

Prev: 03 Backtracking · Next: 05 Greedy

04

Dynamic Programming

Five shapes: Fibonacci ways, 1D skip/take, grid DAG, 0/1 subset sum, two-string LCS. Each card has a simple Java version, an interview version, a tiny example, and a second example that does not skip rows.

Climbing Stairs — recursion → memo → tabulation

Recognize it from

“Number of ways to climb n stairs taking 1 or 2 steps.” Same shape: decode a digit string (one digit or two), tile a 2×n board with 1×2 and 2×1. The clue is ways add, not “max score.”

What it actually does

The last step onto stair n is either a single step from n−1 or a double step from n−2. Those two groups of paths are disjoint, so ways(n) = ways(n−1) + ways(n−2). That is Fibonacci with ways(1)=1, ways(2)=2.

Plain recursion recomputes the same smaller stair over and over — look at climb(2) in Example 1. Memo stores each n once. Tabulation fills 1, 2, 3, … in order. After that you only need two rolling integers, because the recurrence looks two cells back.

Simple — read this first

public int climbStairs(int n) {
    int[] memo = new int[n + 1];
    return climb(n, memo);
}
private int climb(int n, int[] memo) {
    if (n <= 2) return n; // n=1 → one single step; n=2 → 1+1 or 2
    if (memo[n] != 0) return memo[n]; // climb(2) is asked from both climb(4) and climb(3)
    memo[n] = climb(n - 1, memo) + climb(n - 2, memo); // last step was a 1 or a 2 — ways add
    return memo[n];
}

Interview — type this

public int climbStairs(int n) {
    if (n <= 2) return n;
    int prev2 = 1; // ways(1)
    int prev1 = 2; // ways(2)
    for (int i = 3; i <= n; i++) {
        int cur = prev1 + prev2; // only the last two stairs matter
        prev2 = prev1;
        prev1 = cur;
    }
    return prev1;
}
Example 1 — simple

n = 4. Naive recursion (no memo) so you can see the overlap. Five layouts: 1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2.

climb(4) ├─ climb(3) │ ├─ climb(2) = 2 ← first time │ └─ climb(1) = 1 │ climb(3) = 3 └─ climb(2) = 2 ← same question, computed again climb(4) = 3 + 2 = 5
Call tree for n=4 without memo. climb(2) runs twice.
CallDoesReturns
climb(4)climb(3) + climb(2)waits
climb(3)climb(2) + climb(1)waits
climb(2)base n≤22
climb(1)base n≤21
climb(3) returns2 + 13
climb(2) againbase — not reused2
climb(4) returns3 + 25

Answer: 5 ways. With memo, the second climb(2) would read memo[2] and skip the base case.

Example 2 — every step

n = 5, interview rolling variables. Start prev2 = ways(1) = 1, prev1 = ways(2) = 2. Every i from 3 through 5:

Rolling tabulation for n=5. No skipped i.
iprev2 (i−2)prev1 (i−1)cur = prev1 + prev2then prev2, prev1
start12—ways(1), ways(2)
31232, 3
42353, 5
53585, 8

Answer: 8. The eight layouts of five stairs with 1s and 2s. A memo array would hold the same numbers in cells 1…5; the two variables are that array with the old cells thrown away.

Time O(2^n) naive · O(n) memo or rollingSpace O(n) stack+memo · O(1) rolling
Why this

You store answers by stair index because two different paths to stair k share the same suffix. An array (or two ints) is enough — there is no other dimension. Decode-ways is the same recurrence with extra “is this digit legal” guards.

Common mistakes
  • Returning Fibonacci’s F(n) with F(1)=1, F(2)=1. That under-counts n=2 (there are two ways, not one).
  • Memo of size n, then writing memo[n]. Off-by-one: you need index n, so allocate n+1.
  • Thinking the answer is n. Ways are not steps. n=4 is 5 ways.
ProblemPatternLinks
Decode Ways II family (same “1 or 2 at the end”)ways add; extra digit-pair legalitycheat card · clue
“Number of ways” on a grid or a stringdefine the last decision; add incoming countsclue

House Robber — 1D skip / take, then the circle

Recognize it from

“Maximum money / points, cannot take two adjacent.” Linear street, then the follow-up: houses stand in a circle so the first and last are neighbors. Not House Robber on a tree (that is a different recurrence on left/right children).

What it actually does

At house i you either skip it and keep the best of i−1, or take it and add the best of i−2. Those are the only two legal endings. A dp array makes that visible; two rolling integers are the same pair of cells.

On a circle, taking house 0 forbids house n−1. You cannot encode that with one linear pass. Run the linear robber on [0 .. n−2] (drop the last) and on [1 .. n−1] (drop the first), then take the max. n = 1 is a special case: both ranges would be empty if you are not careful.

Simple — read this first

public int rob(int[] a) {
    int n = a.length;
    if (n == 0) return 0;
    if (n == 1) return a[0];
    int[] dp = new int[n];
    dp[0] = a[0];
    dp[1] = Math.max(a[0], a[1]); // two houses: take the richer one
    for (int i = 2; i < n; i++) {
        int skip = dp[i - 1]; // leave a[i] on the table
        int take = dp[i - 2] + a[i]; // a[i-1] is then forbidden
        dp[i] = Math.max(skip, take);
    }
    return dp[n - 1];
}

Interview — type this

public int rob(int[] a) {
    int n = a.length;
    if (n == 0) return 0;
    if (n == 1) return a[0]; // a one-house circle is just that house
    return Math.max(robLine(a, 0, n - 2), robLine(a, 1, n - 1));
}
private int robLine(int[] a, int lo, int hi) {
    int prev2 = 0, prev1 = 0; // best ending two back, one back
    for (int i = lo; i <= hi; i++) {
        int cur = Math.max(prev1, prev2 + a[i]);
        prev2 = prev1;
        prev1 = cur;
    }
    return prev1;
}
Example 1 — simple

Linear street [1, 2, 3, 1]. Best is house 0 + house 2 = 4 (the two 1s at the ends are not adjacent on a line).

index 0 1 2 3 value 1 2 3 1 take 1+3 or 2+1 best 4 3
dp[i] = max(skip dp[i−1], take dp[i−2]+a[i]).
ia[i]skip = dp[i−1]take = dp[i−2]+a[i]dp[i]
01—11
1212 (no i−2 yet, just a[1])2
2321 + 3 = 44
3142 + 1 = 34

Answer: 4.

Example 2 — every step

Circle [2, 3, 2]. Taking both ends is 2+2 = 4 — illegal, because index 0 and index 2 touch. Run both linear ranges. n = 3, ranges are [0..1] and [1..2].

2 ─── 3 │ │ └── 2 ┘ first and last are adjacent
Range A — houses [0..n−2] = [2, 3]. prev2 / prev1 start at 0.
ia[i]skip (prev1)take (prev2+a[i])curthen prev2, prev1
0200 + 2 = 220, 2
1320 + 3 = 332, 3
Range B — houses [1..n−1] = [3, 2]. Same rolling, fresh zeros.
ia[i]skip (prev1)take (prev2+a[i])curthen prev2, prev1
1300 + 3 = 330, 3
2230 + 2 = 233, 3

Answer: max(3, 3) = 3 — the middle house. The illegal 4 never appears in either range, because each range dropped one end.

Time O(n)Space O(n) dp array · O(1) two variables (twice on a circle)
Why this

Adjacent constraint means the state is only “best ending at i.” The circle adds one boolean (did I use house 0?) which splits into two independent lines. Circular Kadane (max wrap-around subarray) is a different circular trick — sums a contiguous block, not skip/take.

Common mistakes
  • Running one linear pass on a circle. That can take both ends.
  • n = 1: calling both ranges and getting max(0, 0). Return a[0].
  • Stamping the tree version (rob left/right with a robbed-parent flag) onto a street.
ProblemPatternLinks
House Robber I → II (circular)skip/take, then max of two linear rangesJava in ch.05 · cheat card · clue
Kadane circular (neighbor, not robber)wrap-around subarray max, not adjacent-skipcheat card
Buy/sell + rain (neighbor)one-transaction running min, not skip-takecheat card

Unique Paths / grid DP — right and down on a DAG

Recognize it from

“Start cell to end cell, only right and down (maybe left/up forbidden), count paths or max score, skip traps.” Obstacles as 1 or −1. Follow-up is often max reward on the same DAG. Four-direction increasing path is not this — that needs memo DFS.

What it actually does

Right and down only means every cell has at most two parents: above and left. The graph is a DAG, so a table in row-major order is a valid topological fill. Path count adds the two parents. Path value takes max or min, then adds this cell. Do not reuse a count table as if it were a score.

A rolling 1D array is the previous row. When you visit column j you overwrite it; read “up” before the write, and “left” from dp[j−1] which is already this row. An obstacle must be written as 0 (count) or a sentinel (max), or the next cell will add a stale number that walked through the block.

Simple — read this first

public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int i = 0; i < m; i++) dp[i][0] = 1; // only down the first column
    for (int j = 0; j < n; j++) dp[0][j] = 1; // only right along the first row
    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]; // ways add from above and left
        }
    }
    return dp[m - 1][n - 1];
}

Interview — type this

public int uniquePathsWithObstacles(int[][] g) {
    int m = g.length, n = g[0].length;
    int[] dp = new int[n]; // dp[j] is "this column, previous row" until we overwrite
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (g[i][j] == 1) {
                dp[j] = 0; // blocked: contributes no paths; must clear a stale up-count
                continue;
            }
            if (i == 0 && j == 0) {
                dp[0] = 1;
                continue;
            }
            int up = i > 0 ? dp[j] : 0; // still the previous row at column j
            int left = j > 0 ? dp[j - 1] : 0; // already this row
            dp[j] = up + left;
        }
    }
    return dp[n - 1];
}
Example 1 — simple

Empty 2×3 grid. Three paths: right-right-down, right-down-right, down-right-right.

(0,0) → (0,1) → (0,2) ↓ ↓ ↓ (1,0) → (1,1) → (1,2) dp row 0: 1 1 1 dp row 1: 1 2 3
Every cell of the 2×3 count table.
cellfrom upfrom leftdp
(0,0)——1 (start)
(0,1)none (first row)11
(0,2)none11
(1,0)1none (first col)1
(1,1)112
(1,2)123

Answer: 3 paths.

Example 2 — every step

3×3 with one obstacle at the center (1,1). Count paths, obstacle = 1. Every cell:

S . . . X . . . E after fill: 1 1 1 1 0 1 1 1 2
Every cell. Obstacle zeros the center; two paths remain (along the top then down, or down then along the bottom).
cellobstacle?upleftdp
(0,0)no——1
(0,1)no0 (no up)11
(0,2)no011
(1,0)no10 (no left)1
(1,1)YESignoredignored0
(1,2)no101
(2,0)no101
(2,1)no011
(2,2)no112

Answer: 2. The four paths that would have stepped on the center die. Max-reward on the same grid is a different recurrence: grid[i][j] + max(up, left), still skipping the zeroed cell — see the notebook max-reward cards, not this count.

Time O(mn)Space O(mn) 2D · O(n) rolling
Why this

Parents of a cell are a constant two (or four on a falling-path DAG). That is a table, not BFS: BFS counts hops, not path combinations. Longest increasing path in four directions is not a DAG from the origin — memoize DFS from each cell instead.

Common mistakes
  • Forgetting to zero an obstacle on a rolling array. The previous row’s count leaks through the block.
  • Using max when they asked for the number of ways, or adding when they asked for max reward.
  • Allowing left/up after you already filled as a right/down DAG — then you need a different graph algo.
ProblemPatternLinks
Unique Paths II / obstacle gridcount, zero obstacles, right/downJava in ch.05 · cheat card · clue
Grid: count paths then max rewardtwo recurrences on the same DAGJava in ch.05 · cheat card · clue · clue · R4 coding
Falling path / Prince right-diag maxDAG max, not path countcheat card · cheat card
Longest increasing path in a matrix4-dir memo DFS — not Unique Paths DPJava in ch.05 · cheat card

0/1 knapsack / partition — boolean subset sum

Recognize it from

“Can you pick a subset that sums to T?” Partition equal subset sum is T = total/2. Target-sum with +/− signs is the same boolean after algebra. If they want the actual subset, keep parent pointers or backtrack when n is tiny — the boolean table only answers yes/no.

What it actually does

Each number may be used at most once. After considering the first i items, dp[i][w] is whether sum w is possible. Transition: skip the item (copy the previous row) or take it if w ≥ x and dp[i−1][w−x] was already true. That previous-row read is what makes it 0/1 rather than unbounded.

The 1D roll overwrites the same array. Looping capacity downward means dp[w−x] still belongs to the previous item. Looping upward lets this same item satisfy dp[w−x] and then immediately satisfy dp[w] — using x twice. Example 2 is that bug.

Simple — read this first

public boolean canPartition(int[] nums) {
    int sum = 0;
    for (int x : nums) sum += x;
    if (sum % 2 != 0) return false; // odd total cannot split in half
    int T = sum / 2;
    int n = nums.length;
    boolean[][] dp = new boolean[n + 1][T + 1];
    for (int i = 0; i <= n; i++) dp[i][0] = true; // sum 0: take nothing
    for (int i = 1; i <= n; i++) {
        int x = nums[i - 1];
        for (int w = 1; w <= T; w++) {
            dp[i][w] = dp[i - 1][w]; // skip this item
            if (w >= x) dp[i][w] = dp[i][w] || dp[i - 1][w - x]; // 0/1: previous row only
        }
    }
    return dp[n][T];
}

Interview — type this

public boolean canPartition(int[] nums) {
    int sum = 0;
    for (int x : nums) sum += x;
    if ((sum & 1) != 0) return false;
    int T = sum / 2;
    boolean[] dp = new boolean[T + 1];
    dp[0] = true;
    for (int x : nums) {
        for (int w = T; w >= x; w--) { // reverse: dp[w-x] is still "without this x"
            dp[w] = dp[w] || dp[w - x];
        }
    }
    return dp[T];
}
Example 1 — simple

nums = [1, 5, 11, 5], total 22, target T = 11. Two yes subsets: {11} and {1,5,5}.

T at columns 0..11. T = true, . = false 0 1 2 3 4 5 6 7 8 9 0 1 0 1 1 after 1 T T . . . . . . . . . . after 5 T T . . . T T . . . . . after 11 T T . . . T T . . . . T after 5 T T . . . T T . . . T T ← dp[11] = T
1D reverse (same truth as the 2D rows). True capacities after each item.
item xloop w = 11 down to xtrue capacities after this item
(start)dp[0] = true{{0}}
1w=1: dp[1] |= dp[0]{{0,1}}
5w=6 |= dp[1]; w=5 |= dp[0]{{0,1,5,6}}
11w=11 |= dp[0]{{0,1,5,6,11}}
5w=11 already T; w=10 |= dp[5]{{0,1,5,6,10,11}}

Answer: true — dp[11] is set after the 11, and stays set.

Example 2 — every step

Why capacity must go down. One item of value 5, capacity 10. 0/1 can make 0 or 5, never 10. Upward reuses the 5.

Correct: reverse, w = 10 down to 5. dp starts [T, F, F, F, F, F, F, F, F, F, F].
wdp[w] beforedp[w−5]dp[w] aftermeaning
10Fdp[5]=FFcannot take 5 twice — 5 is not in the table yet
9Fdp[4]=FF
8Fdp[3]=FF
7Fdp[2]=FF
6Fdp[1]=FF
5Fdp[0]=TTfirst (and only) use of this 5
Wrong: the same item, w = 5 up to 10. Every row.
wdp[w] beforedp[w−5]dp[w] aftermeaning
5Fdp[0]=TTfirst use of 5 — this part is legal
6Fdp[1]=FF
7Fdp[2]=FF
8Fdp[3]=FF
9Fdp[4]=FF
10Fdp[5]=TTWRONG: uses the same 5 a second time

Answer of the bug: dp[10] = true even though you owned only one 5. That is unbounded knapsack. 0/1 must not see a write from the same item. On [1,5,11,5] the same upward pass after the first 5 would mark 10 before the second 5 ever runs.

Time O(nT)Space O(nT) 2D · O(T) 1D
Why this

Boolean on achievable sums is enough for yes/no partition. If they want the combination, store a parent (which item last set dp[w]) or backtrack the table, or switch to recursion when n ≤ 20. Coin change that may reuse a coin is the upward / unbounded loop — opposite of this card.

Common mistakes
  • Looping w from 0 to T for 0/1. That is Example 2.
  • Forgetting the odd-sum early false. You then search a half that is not an integer.
  • Using the boolean table when they asked you to return the subset. Yes is not the list.
ProblemPatternLinks
Partition / target with combination return0/1 subset sum; reconstruct if they want the listJava in ch.05 · cheat card

LCS — two strings, three table moves

Recognize it from

“Longest common subsequence (not substring).” Letters may skip. Related speak: shortest common supersequence = n+m−LCS; delete-only distance = n+m−2·LCS. If they allow replace as well, that is edit distance — a third transition, not max of two.

What it actually does

A subsequence keeps order and may drop letters. dp[i][j] is the LCS of the first i letters of s and the first j of t. Empty prefix is 0, which is why the table is (m+1)×(n+1).

Three moves, only two of which fire on a given cell: if the current letters match, you must take the diagonal + 1 (keeping both letters is never worse than dropping one). If they differ, you drop one letter or the other and take the max. There is no “replace” in LCS. Edit distance adds a third option: cost 1 from the diagonal on a mismatch.

Simple — read this first

public int longestCommonSubsequence(String s, String t) {
    int m = s.length(), n = t.length();
    int[][] dp = new int[m + 1][n + 1]; // extra row/col of zeros for empty prefixes
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s.charAt(i - 1) == t.charAt(j - 1))
                dp[i][j] = dp[i - 1][j - 1] + 1;
            else
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    return dp[m][n];
}

Interview — type this

public int longestCommonSubsequence(String s, String t) {
    int m = s.length(), n = t.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s.charAt(i - 1) == t.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1] + 1; // take the match; skip the letters in between
            } else {
                int skipS = dp[i - 1][j]; // drop s[i-1], keep t's prefix
                int skipT = dp[i][j - 1]; // drop t[j-1], keep s's prefix
                dp[i][j] = Math.max(skipS, skipT); // cannot take a mismatch into the subsequence
            }
        }
    }
    return dp[m][n];
}
Example 1 — simple

s = "ace", t = "abcde". The whole of s sits inside t.

ε 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
Every data cell. Match uses diagonal+1; mismatch uses max(up, left).
i, s[i−1]j, t[j−1]match?transitiondp[i][j]
1 a1 ayesdp[0][0]+11
1 a2 bnomax(0, 1)1
1 a3 cnomax(0, 1)1
1 a4 dnomax(0, 1)1
1 a5 enomax(0, 1)1
2 c1 anomax(1, 0)1
2 c2 bnomax(1, 1)1
2 c3 cyesdp[1][2]+1 = 1+12
2 c4 dnomax(1, 2)2
2 c5 enomax(1, 2)2
3 e1 anomax(1, 0)1
3 e2 bnomax(1, 1)1
3 e3 cnomax(2, 1)2
3 e4 dnomax(2, 2)2
3 e5 eyesdp[2][4]+1 = 2+13

Answer: 3 — ace.

Example 2 — every step

s = "ABCD", t = "ACBD" (length 4). Every cell of the 4×4 interior. LCS length 3 (e.g. ABD or ACD).

ε A C B D ε 0 0 0 0 0 A 0 1 1 1 1 B 0 1 1 2 2 C 0 1 2 2 2 D 0 1 2 2 3
Every interior cell, row-major. No skipped row.
i,sj,tmatch?fromdp
1 A1 Ayesdiag 0+11
1 A2 Cnomax(0,1)1
1 A3 Bnomax(0,1)1
1 A4 Dnomax(0,1)1
2 B1 Anomax(1,0)1
2 B2 Cnomax(1,1)1
2 B3 Byesdiag 1+12
2 B4 Dnomax(1,2)2
3 C1 Anomax(1,0)1
3 C2 Cyesdiag 1+12
3 C3 Bnomax(2,2)2
3 C4 Dnomax(2,2)2
4 D1 Anomax(1,0)1
4 D2 Cnomax(2,1)2
4 D3 Bnomax(2,2)2
4 D4 Dyesdiag 2+13

Answer: 3.

Edit distance, not a sixth algorithm

If they let you insert, delete, or replace, the mismatch cell becomes 1 + min(up, left, diagonal) instead of max(up, left). Classic walk: "horse" → "ros" costs 3 (replace h→r, delete r, delete e). Table bottom-right is 3:

ε 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

Do not mix the two recurrences in one function. LCS never pays for a replace.

Time O(mn)Space O(mn) · can roll to O(min(m,n))
Why this

Each pair of prefixes has one optimal LCS length. The table index is (how much of s, how much of t). Count-of-smaller-after-self is a different table-of-pairs story: merge-sort counts, not a string DP.

Common mistakes
  • Using substring DP (contiguous) when they said subsequence (gaps allowed).
  • On a mismatch, taking the diagonal anyway. That pretends unequal letters matched.
  • Writing edit-distance min into an LCS max table, or the reverse.
ProblemPatternLinks
Count of smaller after self (neighbor)merge-count inversions — not LCSJava in ch.05

See also — same notebook cards, different recurrence

Grid count and grid max-reward share a DAG and different operators (add vs max). House Robber II is the circle split, not Kadane circular. Longest increasing path is memo DFS. Partition that must return the subset is this knapsack plus reconstruction, or the backtracking chapter when n is small.