Study Index
DSA Lesson · Study Guide

Dynamic Programming,
From Recursion Up

The whole topic distilled to one idea: DP is 95% recursion + 5% memoization. Learn the four-step template once, then watch it solve ten problems unchanged.

Top-down & bottom-up 10 worked problems Java pseudocode Interview Q&A
Part 1

Notebook Summary

Hand-copy this into your notebook. Telegraphic by design — every line is derivable from the detailed notes below. Markers (★ ⚠ 💡) sit in the margin so you can box them.

Core idea
DP = two job types only:
  1. OPTIMIZE  → find min / max value
  2. COUNT     → number of ways to reach a goal

DP = RECURSION (95%) + MEMOIZATION (5%)
  memo = just cache the subproblem's answer, reuse it
  effect: time drops exponential → polynomial
The 4-step template (do every problem this way)
1PARAMETERS   → what describes a subproblem?
2BASE CASE    → "what if it doesn't exist?" (smallest input)
3OPTIONS      → at this index: pick / skip (recurse on each)
4COMBINE+CACHE→ count→SUM | optimize→MIN/MAX ; store; return
doodle: 4 stacked boxes labeled PARAM / BASE / OPTIONS / COMBINE, arrow looping COMBINE→PARAM
Memoization: which array?
#changing params → array dimension
  1 changing  → 1-D dp[]
  2 changing  → 2-D dp[][]
  3 changing  → 3-D / HashMap
constants (array itself, n) do NOT count
HashMap key = concat changing params  e.g. i + "," + j
init dp = -1  → "-1 means not computed yet"
doodle: grid of cells, some filled, some "-1", arrow "check before compute"
Top-down vs Bottom-up
Top-down Bottom-up
engine recursion + memo for-loop fills array
start goal, recurse down base index 0 / 0,0 → up
i-2 jump passed as param lookup[i-2]
TC / SC SAME for both
interconvertible — same function, different driver
Pick / skip option pattern
PICK item  → consume capacity/target, add value
SKIP item  → move index, leave capacity
combine = SUM (count) / MAX / MIN (optimize)
guard: if item > remaining → SKIP only (1 option)
Problem cheat-sheet
problem params (var) combine
Coin change (ways) sum, n SUM
Climb stairs n SUM(n-1,n-2,n-3)
House robber i MAX(skip, i-2+v)
Palindrome del. i, j match→recurse; else 1+MIN
0/1 Knapsack W, i MAX(take, skip)
Subset sum sum, n OR (take/skip)
Min partition n, s1 MIN(|s1-s2|)
LIS idx, prevIdx MAX(take, skip)
Decode ways i SUM(1-digit,2-digit)
Inc. path matrix i, j MAX over 4 dirs (DFS+memo)
Margin markers (box these)
★TRICK   stop chasing "winding/unwinding". Trust the
        function: right base + right recurse = right answer.
★TRICK   #changing-params decides 1D/2D/3D. Done.
⚠PITFALL item > target → DON'T return 0. SKIP it.
⚠PITFALL House robber: also dp[i]=max(dp[i], nums[i])
        for all-negative arrays.
⚠PITFALL base: use i==0 (size can't be <0), keep it simple.
💡TIP     LIS prevIdx+1 shifts the -1 sentinel into a
        valid array index.
💡TIP     Greedy = brute force (best-right-now). DP =
        explore ALL options, then min/max. Different beasts.
Part 2

Detailed Study Notes

You're in an interview. The question is "count the ways a child can climb n stairs taking 1, 2, or 3 steps." You write the obvious recursion, it works for n = 5, and then the interviewer says "now n is 40." Your code crawls. The same f(37) is being recomputed thousands of times across different branches, each from scratch. That single wasted recomputation is the entire reason dynamic programming exists — and fixing it is, genuinely, about five lines of code.

The whole thesis here is that DP is not a scary new technique. It's the recursion you already know, with a notepad bolted on. Once you believe that, every problem below becomes a fill-in-the-blanks exercise on one template. We'll build that template first, then run it ten times.

What dynamic programming actually is

Plain English first: dynamic programming is a way of solving a big problem by solving smaller versions of the same problem and remembering the answers so you never solve the same small version twice. The "remembering" is the only thing that separates it from plain recursion.

🧠
The analogy: a smart student doing homework. A naive student re-derives the same intermediate result every time it comes up. A smart student writes each result on a sticky note. Next time that subproblem appears, they read the sticky note instead of re-deriving. The sticky-note pad is your memo (a HashMap or array).

Here's a sharp filter for when a problem is a DP problem. DP solves exactly two kinds of question:

Job type What you're asked Signal words
Optimization Find the best (minimum or maximum) value "minimum", "maximum", "longest", "least"
Counting How many ways to achieve a condition "number of ways", "count", "total ways"
⚠ Pitfall — "minimize" doesn't always mean DP

A common question: "if I see the word minimize, is it always DP?" No. Shortest-path-in-a-grid also says "minimum" but is solved by BFS. And many "best choice" problems are greedy. The real tell is: do you face options where you can't know the right choice locally, forcing you to explore all of them? That's DP. If the locally-best choice is always safe, it's greedy.

💡 Tip — greedy vs DP, the marathon

A mental model: you're running a 50 km marathon and may pick up money once. Greedy grabs the first ₿50 it sees (best right now, "tomorrow doesn't exist"). DP remembers there might be ₿500 at the 25 km mark, so it explores both and takes the max. Greedy = best-at-this-moment brute force; DP = explore every option, then min/max. [+] currency unit illustrative.

The four-step template the whole course in one box

Definition: every DP solution in this session is the same four moves. Memorize this and you stop "thinking about DP" entirely — you just fill in the blanks.

1 · Parameters what defines a subproblem? 2 · Base case "what if it doesn't exist?" 3 · Options pick / skip, recurse each 4 · Combine sum / min / max cache + return recursion repeats steps 2–4 on each smaller subproblem
Fig. 1 — The template. Steps 2–4 are pure recursion; the memo cache lives inside step 4.
  1. Parameters. Ask what uniquely describes one subproblem. Some parameters are constant (the input array, its size n); some change on each call (an index, a remaining sum). Only the changing ones matter.
  2. Base case. A useful trick: ask "what's the smallest/empty version, and what's its answer?" Keep it dead simple.
  3. Options. Standing at the current element, enumerate your choices — almost always pick it or skip it — and make a recursive call for each.
  4. Combine + cache. If counting, sum the option results; if optimizing, take min or max. Store the answer in your memo, then return it.
★ Trick — trust the function, don't trace it

Beginners get stuck mentally "unwinding" the recursion call-by-call. Be blunt with yourself: that approach will sink you in an interview. Instead, trust the significance of the function: "this call returns the correct answer for its subproblem." If your base case is right and your recursive calls are right, the whole thing is right — you don't need to simulate the stack in your head. Trace by hand only once, to believe it; then think only at the current-index level.

Two ways to write it: top-down and bottom-up

Definition: the same DP can be coded two ways. Top-down = recursion that starts at the goal and dives toward the base, caching as it returns. Bottom-up = a loop that starts at the base case and fills a table upward until it reaches the goal.

🧮
Analogy: building a tower. Top-down is asking "to finish floor 10, what do I need from floor 9?" and recursing down. Bottom-up is laying floor 0, then 1, then 2… until floor 10 exists. Same tower, opposite direction.
Top-down (recursion + memo) Bottom-up (loop fills table) f(n) — the goal f(n-1) f(n-2) base f(0) dp[n] — the goal dp[2] dp[1] base dp[0]
Fig. 2 — Same subproblems, opposite traversal. Time and space complexity are identical.
💡 Tip — they're interconvertible, pick what's comfortable

In the house-robber walkthrough the instructor shows the recursive i-2 jump becoming lookup[i-2] in a loop — same logic, the for-loop just plays the role recursion did. Complexity is the same either way. Most people prefer top-down because it's more intuitive; reach for bottom-up if you want to avoid deep call stacks. Either is a correct interview answer.

⚠ Pitfall — "bottom-up avoids stack overflow, so it's faster"

It's tempting to claim bottom-up wins on huge inputs because it skips the call stack. Not so: with a correct base/guard before each recursive call, top-down won't blow the stack unnecessarily, and the time/space complexity is the same. Recursion only goes exponential when you forget memoization — the cache, not the loop, is what saves you.

Choosing the memo: how many dimensions?

The single most useful trick in the session. Beginners freeze on "should this be a 1-D, 2-D, or 3-D array?" The answer: count the parameters that change on each recursive call. That count is your number of dimensions.

Changing parameters Memo structure Example problem
1 dp[] (1-D) Climb stairs (just n)
2 dp[][] (2-D) Knapsack (W, index)
3+ 3-D array or HashMap concat keys into a string

For a HashMap, build the key by concatenating the changing parameters: String key = i + "," + j;. Because those values are unique per subproblem, the key is unique. Constants like the array and its length never count toward the dimension — they don't change, so they can't distinguish subproblems.

★ Trick — initialize the table to −1

Fill your dp array with -1 up front. Then a single check — if (dp[i][j] != -1) return dp[i][j]; — means "already computed, reuse it." That one line is the dynamic-programming part. Everything else is recursion you already know. (Use -1 only when valid answers are non-negative; otherwise use a HashMap's "contains key" check.)


The ten problems — same template, every time

Each problem below follows the identical rhythm: plain-English statement → the four template steps → the code the class arrived at. Notice you're never inventing a new technique — you're choosing parameters and an option set.

Problem 1 — Coin change, count the ways COUNT

Plain English: given coin denominations and a target sum, how many different combinations add up to the target? You have an infinite supply of each coin. For coins = {1,2,3}, sum = 4 the answer is 4: {1,1,1,1}, {1,1,2}, {2,2}, {1,3}.

Template applied

f(sum=4, coin=3) PICK coin 3 f(sum=1, coin=3) · same idx SKIP coin 3 f(sum=4, coin=2) · prev idx answer = PICK + SKIP  (sum the branches)
Fig. 3 — Infinite supply means "pick" keeps the same coin index; "skip" advances. Count = sum of both.
⚠ Pitfall — when a coin exceeds the sum, don't return 0

A student wanted to return 0 the moment a coin is bigger than the remaining sum. Wrong — that kills valid combinations using smaller coins. The correct move is to skip that coin and move to the previous one (the else branch). Returning 0 only happens at a true dead end (sum goes negative or coins run out).

int coinChange(int[] a, int sum, int n, int[][] dp) {
  // base cases
  if (sum == 0) return dp[n][sum] = 1;   // found a way
  if (n == 0)   return 0;                 // no coins left
  if (dp[n][sum] != -1) return dp[n][sum]; // memo hit

  if (a[n-1] <= sum)
    // PICK (same n, infinite supply) + SKIP (n-1)
    return dp[n][sum] = coinChange(a, sum - a[n-1], n, dp) + coinChange(a, sum, n - 1, dp);
  else
    // coin too big — no choice but to skip it
    return dp[n][sum] = coinChange(a, sum, n - 1, dp);
}

Time O(N×sum) · Space O(N×sum)

Why it matters

Problem 2 — Climbing stairs (1, 2, or 3 at a time) COUNT

Plain English: a child climbs n stairs, hopping 1, 2, or 3 steps at a time. How many distinct ways to reach the top? For n = 3 the answer is 4: 1+1+1, 1+2, 2+1, 3.

Template applied

step 0123 (top) +1 +2 +3 f(n) = f(n-1) + f(n-2) + f(n-3)
Fig. 4 — Three hop sizes from each step; total ways is the sum of the three sub-counts.
int climb(int n, int[] dp) {
  if (n == 0) return 1;          // reached top exactly
  if (n < 0)  return 0;          // overshot
  if (dp[n] != -1) return dp[n];  // memo hit
  return dp[n] = climb(n-1, dp) + climb(n-2, dp) + climb(n-3, dp);
}

Time O(n) · Space O(n)

Why it matters

Problem 3 — Max sum of non-adjacent elements (House Robber) OPTIMIZE

Plain English: pick a subset of array elements with the maximum sum, but you may never pick two adjacent elements. For {1,2,9,4,5,0,4,11,6} the best is {1,9,5,11} = 26.

🏠
Analogy: a burglar on a street of houses. Robbing a house trips its neighbors' alarms, so you can't hit two in a row. Maximize loot. (The strong student recognized this as LeetCode's "House Robber".)

Template applied

1 ✓ 2 9 ✓ 4 5 ✓ 0 4 11 ✓ 6 picked {1, 9, 5, 11} = 26 — no two are neighbors
Fig. 5 — Green cells are chosen; never two side-by-side. Each "pick" forces a 2-step jump.
⚠ Pitfall — the all-negatives edge case

The "age condition": if every number is negative, the running best is still negative — but a single large positive element should be allowed to be the answer on its own. So after the usual recurrence, also clamp: dp[i] = max(dp[i], nums[i]). The bottom-up base cases handle size 1 (return nums[0]) and size 2 (max(nums[0], nums[1])) directly.

// bottom-up: lookup[i] = best non-adjacent sum up to index i
int robber(int[] nums) {
  int n = nums.length;
  if (n == 0) return 0;
  if (n == 1) return nums[0];
  int[] lookup = new int[n];
  lookup[0] = nums[0];
  lookup[1] = Math.max(nums[0], nums[1]);
  for (int i = 2; i < n; i++) {
    lookup[i] = Math.max(lookup[i-1], lookup[i-2] + nums[i]); // skip vs pick
    lookup[i] = Math.max(lookup[i], nums[i]);                 // all-negative guard
  }
  return lookup[n-1];
}

Time O(n) · Space O(n)

Why it matters

Problem 4 — Minimum deletions to make a palindrome OPTIMIZE

Plain English: delete the fewest characters so the remaining string reads the same forwards and backwards. "ACBCDBAA" needs 3 deletions.

★ Trick — check palindromes with two pointers, not by reversing

The obvious idea is "reverse it and compare." But there's a better other property: walk a pointer from each end inward — if first == last, both shrink inward. This two-pointer view is what makes the recursion's (i, j) parameters obvious.

Template applied

ACBC DBAA i j match → both move inward (free) · mismatch → delete one end (+1), take min
Fig. 6 — Two pointers from the ends. A matching pair costs nothing; a mismatch costs one deletion.
int minDeletions(String x, int i, int j, Map<String,Integer> memo) {
  if (i >= j) return 0;                 // 0 or 1 char left
  String key = i + "|" + j;
  if (memo.containsKey(key)) return memo.get(key);

  int ans;
  if (x.charAt(i) == x.charAt(j))
    ans = minDeletions(x, i+1, j-1, memo);   // ends match, free
  else
    ans = 1 + Math.min(minDeletions(x, i, j-1, memo),    // drop last
    minDeletions(x, i+1, j, memo));   // drop first
  memo.put(key, ans);
  return ans;
}
Why it matters

Problem 5 — 0/1 Knapsack OPTIMIZE

Plain English: you have items, each with a weight and a value, and a bag that holds at most weight W. Maximize total value without exceeding the capacity. Each item is taken whole or not at all ("0/1").

🧸
Analogy: shopping for mangoes with a 50 kg carry bag. Each mango has a weight and a "value" (say, vitamin C). You can't cut a mango in half — take it whole or leave it. Fill the bag to maximize total vitamin C while staying under the weight limit.

Template applied

item n, capacity W wt[n-1] ≤ W ? TAKE val[n-1] + f(W − wt[n-1], n-1) SKIP f(W, n-1) · capacity unchanged take MAX of the two
Fig. 7 — "Take" adds value and shrinks capacity; "skip" leaves capacity intact. Answer is the max.
⚠ Pitfall — only the "take" branch adds value

A point to watch: in the skip branch you do not add val[n-1], because the item never went in the bag. Adding value in both branches is a classic silent bug.

int knapSack(int W, int[] wt, int[] val, int n, int[][] dp) {
  if (n == 0 || W == 0) return 0;
  if (dp[n][W] != -1) return dp[n][W];
  if (wt[n-1] > W)                          // too heavy → must skip
    return dp[n][W] = knapSack(W, wt, val, n-1, dp);
  return dp[n][W] = Math.max(val[n-1] + knapSack(W - wt[n-1], wt, val, n-1, dp), // take
    knapSack(W, wt, val, n-1, dp));                     // skip
}

Time O(N×W) · Space O(N×W)

Why it matters

Problem 6 — Subset sum (does one exist?) DECISION

Plain English: given non-negative integers and a target, is there any subset that adds up exactly to the target? {3,34,4,12,5,2}, target 9 → true (4+5). Target 30 → false.

Template applied

int subsetSum(int[] a, int n, int sum, int[][] tab) {
  if (sum == 0) return 1;          // found target
  if (n <= 0)   return 0;          // out of elements
  if (tab[n-1][sum] != -1) return tab[n-1][sum];
  if (a[n-1] > sum)                  // can't include
    return tab[n-1][sum] = subsetSum(a, n-1, sum, tab);
  // include OR exclude
  boolean ok = subsetSum(a, n-1, sum, tab) != 0 || subsetSum(a, n-1, sum - a[n-1], tab) != 0;
  return tab[n-1][sum] = ok ? 1 : 0;
}

Time O(N×sum) · Space O(N×sum)

💡 Tip — stay at the current element

It's tempting to keep reaching for "the next element." Instead, focus only on the current element's two choices. The next element takes care of itself when it becomes the current element in the recursive call. Thinking one index at a time is the whole point of the "trust the function" trick.

Problem 7 — Minimum subset-sum difference OPTIMIZE

Plain English: split a set into two groups so the difference between their sums is as small as possible; return that minimum difference. {10,20,15,5,25} splits into sums 35 and 40 → difference 5.

Template applied

★ Trick — one variable defines both groups

Because every element goes to exactly one group, S2 = totalSum − S1. So the unique key is just (n, S1) — tracking S2 too would be redundant and inflate your state space. This "the rest is implied" insight collapses a 3-D-looking state into 2-D.

int minDiff(int[] S, int n, int s1, int s2, Map<String,Integer> memo) {
  if (n < 0) return Math.abs(s1 - s2);
  String key = n + "|" + s1;          // s2 is implied
  if (memo.containsKey(key)) return memo.get(key);
  int inc = minDiff(S, n-1, s1 + S[n], s2, memo);   // → group 1
  int exc = minDiff(S, n-1, s1, s2 + S[n], memo);   // → group 2
  int ans = Math.min(inc, exc);
  memo.put(key, ans);
  return ans;
}

Time O(N×sum) · Space O(N×sum)

Why it matters

Problem 8 — Longest increasing subsequence (LIS) OPTIMIZE

Plain English: find the length of the longest subsequence whose elements are in strictly increasing order. {3,10,2,1,20} → {3,10,20}, length 3.

🏏
Analogy: comparing cricketers' run totals. To decide if 10 extends a sequence, you compare it to the last player you kept (say 3). The "previous kept value" is the yardstick — that's exactly what prevIdx tracks.

Template applied

💡 Tip — the prevIdx + 1 indexing shift

prevIdx starts at -1 (nothing kept yet), but arrays can't be indexed at -1. So the memo is indexed as dp[idx][prevIdx + 1] — the +1 shifts the range to start at 0. This detail matters and plays a critical role.

3 10 2 1 20 prevIdx (last kept) idx (current) take 20 only if 20 > a[prevIdx]=10 → kept: {3,10,20} = 3
Fig. 8 — prevIdx remembers the last element added so the increasing rule can be enforced.
int f(int idx, int prevIdx, int n, int[] a, int[][] dp) {
  if (idx == n) return 0;
  if (dp[idx][prevIdx + 1] != -1) return dp[idx][prevIdx + 1];
  int skip = f(idx + 1, prevIdx, n, a, dp);
  int take = Integer.MIN_VALUE;
  if (prevIdx == -1 || a[idx] > a[prevIdx])
    take = 1 + f(idx + 1, idx, n, a, dp);   // current becomes new prev
  return dp[idx][prevIdx + 1] = Math.max(take, skip);
}

Time O(n²) · Space O(n²)

⚠ Pitfall — sorting into a Set destroys the answer

One tempting idea is dumping elements into a sorted Set to find the increasing run. That fails: a Set reorders elements, but a subsequence must preserve the original order. Reordering changes the problem entirely.

Problem 9 — Decode ways (digits → letters) COUNT

Plain English: with mapping 1→A, 2→B, … 26→Z, count how many ways a digit string can be decoded. "123" → 3 (ABC, AW, LC); "1221" → 5.

Template applied (bottom-up)

2 3 single '3' → valid (C) → + T[i-1] pair '23' ≤ 26 → valid (W) → + T[i-2]
Fig. 9 — At each digit, sum two possibilities: decode it alone, and (if 10–26) decode it paired with the previous digit.
int count(int x) {
  String seq = String.valueOf(x);
  int[] T = new int[seq.length() + 1];
  T[0] = 1; T[1] = 1;
  for (int i = 2; i <= seq.length(); i++) {
    if (seq.charAt(i-1) > '0')                 // single digit 1–9
        T[i] = T[i-1];
    if (seq.charAt(i-2) == '1' ||              // two-digit 10–26
        (seq.charAt(i-2) == '2' && seq.charAt(i-1) <= '6')) T[i] += T[i-2];
  }
  return T[seq.length()];
}

Time O(n) · Space O(n)

Why it matters

Problem 10 — Longest increasing path in a matrix OPTIMIZE

Plain English: in an n×n grid of distinct numbers, find the longest path where each step moves to an adjacent cell (up/down/left/right) whose value is exactly one greater. The path 6→7→8→9 gives length 4.

Template applied (DFS + memo)

1 2 9 5 3 8 4 6 7 6→7→8→9 length 4
Fig. 10 — Each move must go to a neighbor exactly one greater. Memoize each cell's best path length.
★ Trick — it's just DFS with one extra condition

This connects to the earlier grid/BFS material: it's a standard 4-direction DFS, plus the rule "only step where neighbor − current == 1," plus memoization on each cell. Recognizing a "new" problem as a known traversal with one added guard is the meta-skill the whole session is teaching.

int dfs(int[][] m, int i, int j, Map<String,Integer> memo) {
  String key = i + "," + j;
  if (memo.containsKey(key)) return memo.get(key);
  int best = 1;                              // the cell itself
  int[][] dirs = {{0,1},{1,0},{0,-1},{-1,0}};
  for (int[] d : dirs) {
    int r = i + d[0], c = j + d[1];
    if (r >= 0 && r < m.length && c >= 0 && c < m.length && m[r][c] - m[i][j] == 1)            // the extra rule
        best = Math.max(best, 1 + dfs(m, r, c, memo));
  }
  memo.put(key, best);
  return best;
}
⚠ Pitfall — the memo key is the position, not the value

It's tempting to guess the HashMap key should be the cell's value. It isn't: the key is the index (i, j), because that's what changes per subproblem. Values happen to be distinct here, but coordinates are the honest state.

Part 3

Interview & Practice Q&A

The follow-ups this material naturally invites. Each answer is short enough to say out loud. Click to expand.

Q1 How do you decide whether a problem needs dynamic programming at all? ❯

Look for two signatures: you're asked for a min/max (optimization) or a count of ways, and you face choices where the locally-best option isn't provably correct, forcing you to explore all options. If the locally-greedy choice is always safe it's greedy; if there's a clear graph distance it may be BFS/Dijkstra. When unsure, try two or three tiny examples by hand and see whether sub-results repeat.

Q2 What's the precise relationship between recursion and DP? ❯

DP is recursion plus memoization — roughly "95% recursion, 5% memo." You write the same recursive solution (parameters, base case, options, combine), then add one cache check so each unique subproblem is computed only once. That single change drops the time complexity from exponential to polynomial without altering the logic.

Q3 How do you know how many dimensions your memo array needs? ❯

Count the parameters that change between recursive calls. One changing parameter → 1-D array; two → 2-D; three or more → a multi-dimensional array or a HashMap keyed by the concatenated changing parameters. Constants like the input array and its length never count, because they can't distinguish one subproblem from another.

Q4 Top-down or bottom-up — which should you use? ❯

Either; they have identical time and space complexity and are mechanically interconvertible. Top-down (recursion + memo) is usually more intuitive and easier to derive from the recurrence, so it's a safe default. Bottom-up (iterative table filling) avoids deep call stacks, which can matter for very large inputs. State that they're equivalent and pick whichever you can implement cleanly under pressure.

Q5 When an item is too big for the remaining capacity/target, why not just return 0? ❯

Because smaller items further along may still complete a valid solution. Returning 0 throws those away. The correct move is to skip the oversized item and recurse on the previous index, leaving the capacity unchanged. You only return a true failure value (0 or false) at a genuine dead end — the target goes negative, or you run out of items.

Q6 In 0/1 knapsack, why does only the "take" branch add the item's value? ❯

Value is the reward for putting an item in the bag. In the "skip" branch the item never enters the bag, so there's nothing to add — you just carry the capacity forward unchanged. Adding the value in both branches is a common bug that silently inflates the answer.

Q7 In LIS, what is prevIdx for and why the +1 in the memo index? ❯

prevIdx holds the index of the last element you kept, so you can enforce "strictly increasing" by comparing the current element against a[prevIdx]. It starts at -1 (nothing kept yet), and since arrays can't be indexed at -1, the memo uses dp[idx][prevIdx + 1] to shift the valid range to start at 0.

Q8 Does converting recursion to a loop change the complexity? ❯

No. Both forms visit the same set of subproblems exactly once thanks to memoization, so time and space are identical. The difference is only the driver: recursion descends from the goal and caches on the way back; the loop builds the same table forward from the base case. A correctly memoized top-down solution won't overflow the stack any more than necessary.

Q9 How is greedy different from DP if both can say "min" or "max"? ❯

Greedy commits to the best-looking choice at each step and never reconsiders — it's essentially structured brute force, valid only when the local optimum guarantees the global one. DP explores all options for each decision and then takes the min/max, precisely because you can't trust a single local choice. If a counterexample shows greedy failing on small inputs, you need DP.

Part 4

Key Takeaways

Carry these into every DP problem

  1. DP = recursion + memoization. If your recursion is solid, DP is one cache line away. Stop treating it as a separate, scary technique.
  2. Run the four-step template every time: parameters → base case → options (pick/skip) → combine (sum for counting, min/max for optimizing) and cache.
  3. Count the changing parameters to choose 1-D / 2-D / 3-D / HashMap. Constants don't count.
  4. Trust the function. Don't simulate the call stack in your head; if base case and recursive calls are correct, the result is correct. Trace by hand only once, to build belief.
  5. Think at the current element only. Its choices are pick or skip; the next element handles itself when it becomes "current."
  6. Watch the recurring traps: skip (don't return 0) oversized items; only "take" adds value; clamp for all-negative arrays; the memo key is the changing state, not the cell value.
  7. Top-down and bottom-up are the same answer with identical complexity — pick the one you can write cleanly.

Compiled and refined into a single reference on dynamic programming. Filler removed; garbled terms reconciled against the code. Items marked [+] are clarifying additions not in the sources; ! would flag a source conflict (none material here). Related topics: backtracking and trie data structures — both recursion-heavy.