DP Foundations: The Two Pillars and Five Questions¶
Phase 4 | Week 24
Before you solve a single DP problem, you need these mental models locked in. Everything else in this phase is an application of what’s on this page. Read it carefully. Come back to it when you’re stuck on a problem and can’t figure out the state.
The Two Pillars of DP¶
DP applies when a problem has both of the following properties:
1. Overlapping Subproblems¶
The same smaller problem needs to be solved more than once during the computation. This is what distinguishes DP from divide-and-conquer.
Divide and conquer (Merge Sort, Quick Sort): Subproblems are independent. Merging two halves never reuses work from the other half. No caching needed.
DP (Fibonacci, Coin Change): Subproblems overlap. To compute
fib(5), you computefib(3)twice if you’re naive. Caching saves exponential repeated work.
If your recursion tree has no repeated nodes, it’s not a DP problem — it’s D&C.
2. Optimal Substructure¶
The optimal solution to the whole problem can be constructed from optimal solutions to subproblems. You don’t need to re-examine subproblem solutions once they’re computed — the best answer to a subproblem is always useful for the whole problem.
Example: Shortest path in a graph has optimal substructure. If the shortest path from A to C goes through B, then the A→B segment must also be the shortest path from A to B. If it weren’t, you could replace it with a shorter one and get a shorter A→C path — contradiction.
Counterexample: Longest simple path does NOT have optimal substructure, which is why it’s NP-hard while shortest path is polynomial.
Top-Down: Memoization¶
Start with the natural recursive solution. Add a cache. Done.
memo = {}
def solve(i, ...):
if (i, ...) in memo:
return memo[(i, ...)]
# base case
if i == 0:
return base_value
# recursive case
result = some_function(solve(i-1, ...), ...)
memo[(i, ...)] = result
return result
When to use top-down:
The recursion is naturally easy to express
Not all subproblems need to be solved (sparse subproblem space)
You want to write the solution quickly and verify correctness before optimizing
Drawback: Recursive call stack overhead. In languages without tail-call optimization (Java, C++), deep recursion can stack overflow.
Bottom-Up: Tabulation¶
Fill a table starting from base cases, working up to the answer. No recursion.
dp = [0] * (n + 1)
dp[0] = base_value # base case
for i in range(1, n + 1):
dp[i] = some_function(dp[i-1], ...)
return dp[n]
When to use bottom-up:
All subproblems must be solved anyway
You want to avoid stack overflow risk
You want to optimize space (you can discard old table entries)
Drawback: You must figure out the correct iteration order. If dp[i] depends on dp[j] where j > i, you can’t iterate left to right.
Space Optimization¶
When your recurrence only looks back a fixed number of steps, you don’t need to store the entire table.
Fibonacci: fib(n) = fib(n-1) + fib(n-2) only needs the last two values.
# O(n) space (full table)
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
# O(1) space (rolling variables)
a, b = 0, 1
for _ in range(n-1):
a, b = b, a + b
return b
2D → 1D optimization: For problems where dp[i][j] only depends on dp[i-1][j] and dp[i][j-1], you can keep only the current and previous rows — O(cols) space instead of O(rows × cols).
State Design: The Hardest Skill¶
State design is the answer to: “What does dp[i] (or dp[i][j]) actually mean?”
A well-defined state must:
Fully capture everything you need to make a decision at that point
Not include redundant information
Lead to a clean transition (the recurrence relation)
The single most common DP mistake is defining the state too vaguely. If you define dp[i] as “something about the first i elements” without being precise, your transitions will be wrong.
Bad: dp[i] = “answer for the first i elements” (what does “answer” mean? best sum? count of ways? can we reach here?)
Good: dp[i] = “maximum sum of a subarray ending at index i” or “number of ways to make change for amount i using the first j coin types”
Two Examples of State Definition Errors¶
Error 1 — House Robber gone wrong:
Suppose you define dp[i] = “maximum money robbing from houses 0..i” without tracking whether house i was robbed.
Your transition wants to say: if I rob house i, I must skip house i-1
But
dp[i-1]tells you the max over houses 0..i-1, which might include house i-1You can’t determine whether the last house was robbed
Fix: Either define dp[i] = “max money from 0..i where house i IS robbed” and dp2[i] = “max money where house i is NOT robbed”, or recognize that dp[i] = max(dp[i-2] + nums[i], dp[i-1]) already encodes this correctly.
Error 2 — Knapsack off by one:
Students frequently define dp[w] = “max value with capacity exactly w” when they mean “max value with capacity at most w”. These have different base cases. dp[0] = 0 in both cases, but the transition dp[w] = max(dp[w], dp[w - weight[i]] + value[i]) is valid for “at most” but incorrect if interpreted as “exactly” (because dp[w - weight[i]] might be -infinity if no combination reaches exactly that weight).
The 5 Questions to Ask Every DP Problem¶
Apply these in order before writing a single line of code:
What are the states? What information do I need at each step? What indices, counts, or flags define a unique subproblem?
What are the transitions? How does the answer for state
(i, j, ...)depend on smaller states? Write the recurrence explicitly.What are the base cases? What are the simplest subproblems whose answers are obvious? (Empty array, zero items, zero capacity, etc.)
What is the answer? Which state holds the final answer? Is it
dp[n]?dp[n][W]?max(dp[i])over alli?Can I optimize space? Does my recurrence only look back a constant number of steps? If so, I can reduce from O(n²) table to O(n) or O(1).
Write the answers to all 5 questions before coding. If you can’t answer question 2, you don’t understand the problem yet.
What Most Engineers Get Wrong¶
The failure pattern is almost always the same: they skim a solution, code it up, submit, pass, and move on. Two weeks later, they can’t reproduce it. They memorized the answer, not the reasoning.
The correct workflow:
Identify the pattern (1D? knapsack? 2D grid?)
Define state explicitly in writing
Derive the transition from first principles
Identify base cases
Code it
If you always do step 2 and 3 in writing before touching a keyboard, your DP accuracy will roughly triple.
Practice Before Moving On¶
Before proceeding to 02_1d_and_linear_dp.md, be able to answer these from memory:
What two properties must a problem have for DP to apply?
What’s the difference between memoization and tabulation?
What are the 5 questions to ask any DP problem?
Give an example of a bad state definition and explain why it breaks the transition
Navigation ← Phase 4 README | Next: 1D Linear DP →