DP — From Recursion Up
A companion take on the same topic: DP as 95% recursion + 5% memoization. One four-step template, taught recursion-first, then applied unchanged to ten worked problems — coin change, knapsack, subset sum, LIS, decode ways and more.
Assumes you know
After this you should be able to
- Explain the core idea in DP — From Recursion Up
- Apply DP — From Recursion Up in a representative exercise
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.
Prefer a problem-first reference with LeetCode links and a 3-step framework? See the companion notes: Dynamic Programming — Complete Study Guide.
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.
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
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
#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"| 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 | |
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 | 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) |
★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.
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.
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" |
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.
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.
- 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. - Base case. A useful trick: ask "what's the smallest/empty version, and what's its answer?" Keep it dead simple.
- Options. Standing at the current element, enumerate your choices — almost always pick it or skip it — and make a recursive call for each.
- Combine + cache. If counting, sum the option results; if optimizing, take min or max. Store the answer in your memo, then return 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.
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.
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.
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#
- Parameters (changing): remaining
sumand coin indexn→ two changing → 2-D memo. - Base cases:
sum == 0→ return 1 (one valid way found);n == 0(no coins left) → return 0. - Options: if this coin fits (
a[n-1] ≤ sum): pick it — stay on the same index (infinite supply!) and reduce the sum — plus skip it — move to the previous coin, sum unchanged. - Combine: it's a counting problem, so add the two option results.
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)
- This is the canonical "infinite supply" shape: "pick" doesn't advance the index. Compare it to knapsack/subset-sum where each item is used at most once and "pick" does advance.
- The brute-force version recomputes the same
(sum, coin)states over and over — the class noted the call stack "grows exponentially" without the memo.
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#
- Parameter (changing): just
n, the steps remaining → one changing → 1-D memo. - Base cases:
n == 0→ return 1 (reached the top exactly — one valid path);n < 0→ return 0 (overshot). - Options: three of them — recurse on
n-1,n-2,n-3. - Combine: counting → add all three.
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)
- This is the gentlest possible DP — one parameter, pure counting. It's the Fibonacci recurrence
with three terms, and the instructor uses Fibonacci's repeated
f(2)to show why memoization is needed. - Adding the explicit
n < 0guard "to be safe" is cleaner than only guarding inside each recursive call.
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.
Template applied#
- Parameter (changing): index
i→ 1-D. - Options: pick element
i→ addnums[i]and jump toi-2(skip the neighbor); skip → move toi-1. - Combine: optimizing → take the max.
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)
- This is the clearest set-piece for showing top-down ↔ bottom-up equivalence: the recursive
i-2jump becomeslookup[i-2], thei-1skip becomeslookup[i-1]. - The "pick forces a 2-jump" idea recurs in many interval/sequence DPs; recognize it.
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.
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#
- Parameters (changing): start index
iand end indexj→ two changing → 2-D memo (or HashMap keyedi+"|"+j). The string and its length are constants. - Base case:
i ≥ j(pointers met or crossed) → return 0. - Options: if
s[i] == s[j], no deletion needed — recurse on(i+1, j-1). Otherwise you must delete one end:1 + min(delete-last(i, j-1), delete-first(i+1, j)).
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; }
- First genuinely two-parameter problem — it cements why "count the changing params" gives 2-D here.
- The
1 + min(...)shape (each operation costs 1, minimize the total) is the edit-distance family. Recognizing it unlocks Levenshtein, longest-common-subsequence, etc.
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").
Template applied#
- Parameters (changing): remaining capacity
Wand item indexn→ 2-D. One index serves both the value and weight arrays. - Base case:
n == 0orW == 0→ return 0. - Options: if the item is too heavy (
wt[n-1] > W) you can only skip. Otherwise choose the max of: take — addval[n-1], reduce capacity bywt[n-1], move ton-1— vs skip — move ton-1, capacity unchanged.
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)
- 0/1 Knapsack is the most-asked DP archetype. "Take advances the index AND reduces capacity" distinguishes it from the infinite-supply coin problem.
- By this point the strong student could "read the code without writing it" — the template had become reflex.
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#
- Parameters (changing): remaining
sumand indexn→ 2-D. Each element used at most once (no infinite supply). - Base cases:
sum == 0→ true (1);n ≤ 0→ false (0). - Options: if
a[n-1] > sumyou must exclude. Otherwise it's an OR of include (sum - a[n-1], indexn-1) and exclude (sum, indexn-1). - Combine: a decision problem — logical OR (true if either branch succeeds).
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)
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#
- Parameters (changing): index
nandS1(sum collected in group 1) → 2-D. You don't needS2separately — it'stotalSum − S1. - Base case:
n < 0→ return|S1 − S2|. - Options: put the current element in group 1 (
S1 + a[n]) or group 2 (S2 + a[n]). - Combine: take the min of the two resulting differences.
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)
- The "every option returns the thing the question asks for, so you can min/max over options" mantra is crystal clear here: each recursive call returns a final difference, and you simply take the smallest.
- Reducing state with "the rest is implied" is a reusable space-saving move across partition/balance problems.
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.
prevIdx tracks.Template applied#
- Parameters (changing): current index
idxandprevIdx(the index of the last element kept) → 2-D. - Base case:
idx == n→ return 0 (nothing left to extend). - Options: skip the current element (
idx+1, sameprevIdx); or, only ifprevIdx == -1ora[idx] > a[prevIdx], take it (1 + f(idx+1, idx)). - Combine: max of take and skip.
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.
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²)
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)#
- Parameter (changing): position
iin the string → 1-D tableTwhereT[i]= ways to decode the firsticharacters. - Base case:
T[0] = 1(empty string, one way) andT[1] = 1. - Options at each
i: if the single digit> 0, it decodes alone → addT[i-1]. If the previous two digits form 10–26, that pair decodes too → addT[i-2]. - Combine: counting → sum the valid one-digit and two-digit contributions.
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)
- It's the counting twin of climbing stairs — "
T[i] = T[i-1] + T[i-2]" — but each term is gated by a validity check (digit > 0; pair within 10–26). Conditional Fibonacci. - Written bottom-up to show, again, that the loop form and the recursion form are the same recurrence.
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)#
- Parameters (changing): cell coordinates
(i, j)→ HashMap keyed"i,j"(effectively 2-D). The matrix is constant. - Base case: a cell always contributes at least length 1 (itself).
- Options: try all 4 directions; recurse only into a neighbor whose value is
current + 1and within bounds. - Combine: max over the four directions; cache and return.
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; }
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.
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.
Carry these into every DP problem#
- DP = recursion + memoization. If your recursion is solid, DP is one cache line away. Stop treating it as a separate, scary technique.
- Run the four-step template every time: parameters → base case → options (pick/skip) → combine (sum for counting, min/max for optimizing) and cache.
- Count the changing parameters to choose 1-D / 2-D / 3-D / HashMap. Constants don't count.
- 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.
- Think at the current element only. Its choices are pick or skip; the next element handles itself when it becomes "current."
- 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.
- 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.