A free Dynamic Programming guide for coding interview prep on NudgeCode: For house values [2,7,9], adjacent houses cannot both be taken. Skip the last house and reuse the first two houses’ answer, or take it and combine with the first house. Explain these choices before writing states.
What you will be able to do
- Define each state in a sentence
- Derive transitions from choices
- Compute dependencies first and locate the result
Read first: Backtracking
Dynamic Programming
House values are [2, 7, 9], and two adjacent houses can't both be robbed. Look at the last house (9); there are two choices. Skip it, and the answer is the best for the first two houses alone, 7. Rob it, and the second house is off limits, so add it to the best for the first house alone, 2: 2 + 9 = 11. The larger is 11. Spell out these two choices before writing the state and the transition.
Dynamic programming stores answers to repeated subproblems so later work can reuse them. Top-down memoization computes a state recursively and caches its result. Bottom-up tabulation computes dependencies before the states that need them. Both start with a precise state definition and recurrence.
Every solution has to respect three principles:
- The definition of
dp[i]decides everything. First write, in one sentence, whatdp[i]means: ending at i, the prefix of length i, starting from i, or the range[i, j]. The base case, the induction rule, the fill order and where the answer lives all follow from that sentence. Change the definition and all four change with it. - Before you fill a cell, every cell it depends on must already be filled. If the induction rule looks left, fill left to right; if it looks right, fill right to left. When the order is hard to state, hand it to recursion + memo and let the call stack sort it out.
- Every possible solution must land in some cell, and counting problems must not count anything twice. "Must end at i", "enumerate where the last cut goes" and "consider coins in a fixed order" all exist so that every case is covered exactly once.
The article goes in this order, and each step adds exactly one idea:
| Step | New idea | Representative problems |
|---|---|---|
| Step 1 | A table is a recursion that remembers its results; look back one or two cells | 70, 198 |
| Step 2 | Define dp[i] as "ending at i", and the answer is the max over the table | longest ascending subarray, 53, 152 |
| Step 3 | Same problem, different definition, and the fill direction flips | 55, 45, 329 |
| Step 4 | Look back at every j < i: read the left big part from the table, compute the right small part directly | 343, 139, 300 |
| Step 5 | Two indices dp[i][j]; look back left, up, and up-left | 1143, 72, 221 |
| Step 6 | Knapsack: one 1D table where loop order decides how many times an item can be used, and whether you count combinations or permutations | 416, 322, 518 |
| Advanced | When one cell can't hold enough, add a dimension to the state: state machine, interval, game | 309, 647, 877, 312 |
When to Use This Pattern
The brute force is usually a recursion tree that tries every choice at every step (rob or skip, where to jump, where the last cut goes). It is slow because the same argument gets computed many times. Take Fibonacci, f(5) = f(4) + f(3), and count how often each argument is called:
| Argument | f(5) | f(4) | f(3) | f(2) | f(1) | f(0) |
|---|---|---|---|---|---|---|
| Calls without memo | 1 | 1 | 2 | 3 | 5 | 3 |
| Calls with memo | 1 | 1 | 1 | 1 | 1 | 1 |
Without memo the number of calls is O(2^n); with memo each argument is computed once, O(n). A quick way to estimate any DP: number of states × cells each state looks back at × cost of looking at one cell.
| Signal in the problem | Which step |
|---|---|
| Max / min / count / feasible, and each step only looks at the previous one or two positions | Step 1 |
| Longest / largest contiguous subarray or substring | Step 2 |
| Can you reach the end from here, and in how few moves | Step 3 |
| Cut into pieces, break into dictionary words, subsequences (not contiguous) | Step 4 |
| Compare, match or edit two strings, or walk a grid, find a square | Step 5 |
| A capacity / amount limit and items to choose; can you make it, how few, how many ways | Step 6 |
| Several mutually exclusive states per day, an answer that depends on a range, two players taking turns | Advanced |
Recognition Checklist
- The problem asks for an optimum, a count, or true/false, not a list of every solution
- The answer to the big problem can be assembled from answers to problems one size smaller
- When you draw the recursion tree, the same argument shows up more than once
- The state fits in one or two indices: a position, a length, two prefixes, a range, remaining capacity
- Each step has only a few choices: take or skip, where to cut, match or not
- Input size is around 10^3 to 10^4, so O(n²) or O(n·m) is fine
Before You Code
Before writing code, answer these four questions on paper. The first three are the DP definition, base case and induction rule; the fourth, "where is the answer", gets its own line because it is the easiest one to get wrong. Once the four answers are written down, the code is mostly transcription.
- What does
dp[i]mean? One plain sentence. The common shapes are: ending at i (must include i), the prefix of length i, and starting from i. If i is an index the array has n cells. If i is a length it has n + 1, because a length can be any of 0..n, which is n + 1 values; the extradp[0]is the empty prefix, and the last element covered bydp[i]iss[i-1]. - Which cells are the base case? The smallest ones, which you can write down without reading any other cell, such as
dp[0],dp[0][j],dp[i][0]. - Which cells does the induction rule look back at? Say it in words first, then as a formula. The direction you look back decides the fill order.
- Which cell holds the answer? If the definition is a prefix, or the value only ever carries forward and never restarts, the answer is in the last cell. If the definition says "ending at i" and the value can restart, the answer is the maximum over the whole table.
The four answers form a chain: once the sentence in question 1 is clear, question 2 is that sentence at the smallest size, question 3 is that sentence going from one size smaller to the current size, and question 4 depends on whether the sentence says "must include i".
Try it on the longest ascending subarray: a = [7, 2, 3, 1, 5, 8, 9, 6]. The longest contiguous ascending run is [1, 5, 8, 9], so the answer is 4.
- Meaning:
dp[i]= length of the longest ascending subarray that ends ata[i]and must include it. Why must it includea[i]? A subarray is contiguous, soa[i]can only extend the run that ends ata[i-1]. Ifdp[i-1]didn't promise to end ata[i-1], you couldn't tell whethera[i]connects to it. - Base case:
dp[0] = 1, a single number. - Induction rule: if
a[i] > a[i-1], extend the previous run,dp[i] = dp[i-1] + 1; otherwise start over at i,dp[i] = 1. It only looks one cell to the left, so fill left to right. - Answer: the value can restart, so the longest run doesn't have to end at the last number (here
dp[7] = 1). The answer ismax(dp).
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| a[i] | 7 | 2 | 3 | 1 | 5 | 8 | 9 | 6 |
| dp[i] | 1 | 1 | 2 | 1 | 2 | 3 | 4 | 1 |
Step 2 turns this into code.
House Robber (LeetCode 198)
Given a line of nonnegative house amounts, return the maximum total from nonadjacent positions, allowing no selections; do not return indices. Adjacency follows the original order. The first and last houses are not neighbors in this linear version.
Input: nums = [2, 1, 4, 5]. Expected: 7 (houses 0 and 3).
dp[i] = the most money you can take from houses 0..i, with no two adjacent houses. For house i, either skip it (dp[i-1]) or take it (dp[i-2] + nums[i]).
1. Walk it by hand
| Step | i | nums[i] | Skip i: dp[i-1] | Take i: dp[i-2] + nums[i] | dp[i] | What we do / why |
|---|---|---|---|---|---|---|
| 1 | 0 | 2 | – | – | 2 | Base case: only one house, take it |
| 2 | 1 | 1 | – | – | 2 | Base case: two houses, take the richer one, max(2, 1) |
| 3 | 2 | 4 | 2 | 2 + 4 = 6 | 6 | Taking house 2 is better |
| 4 | 3 | 5 | 6 | 2 + 5 = 7 | 7 | Taking house 3 (with house 0) is better |
| 5 | end | – | – | – | – | Answer is the last cell: dp[3] = 7 |
2. Read the rule off the table
- The first two cells are filled directly:
dp[0] = nums[0],dp[1] = max(nums[0], nums[1]). - From
i = 2on, every row compares two numbers, skip (dp[i-1]) and take (dp[i-2] + nums[i]), and keeps the larger one. - Each row only reads cells to its left, so filling left to right always works. The answer is the last cell.
3. Turn each row into code
| Trace step | Python |
|---|---|
| One house only | if len(nums) == 1: return nums[0] |
| Table with one cell per house | dp = [0] * len(nums) |
| Rows 1–2: base cases | dp[0] = nums[0] / dp[1] = max(nums[0], nums[1]) |
| Rows 3–4: fill left to right | for i in range(2, len(nums)): |
| Skip vs take | dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) |
| Row 5 | return dp[-1] |
def rob(nums):
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]
Core Templates
Every step uses the same approach: answer the four questions above, then check the three principles from the top. Can the definition be said in one sentence? Are the cells you depend on already filled? Did you miss any case?
Step 1: 1D linear, look back one or two cells
Problem: House Robber (LC 198). A row of houses; you can't rob two adjacent ones. What's the most you can take?
The input is an array nums of nonnegative integers, where nums[i] is the money in house i. Return the largest total you can take without robbing two adjacent houses. Return the amount, not house indices. Robbing nothing is allowed.
For example, [2, 7, 9, 3, 1] → 12: rob houses 0, 2 and 4, 2 + 9 + 1 = 12.
LeetCode guarantees a nonempty array. Below, rob_memo also returns 0 for an empty array, while rob requires a nonempty one.
The questions:
- Meaning:
dp[i]= the most you can take from houses0..i(house i included in the range) without robbing two adjacent ones. It does not require robbing house i. i is an index, so the array has n cells. - Base case:
dp[0] = nums[0], one house, rob it;dp[1] = max(nums[0], nums[1]), two houses, you can rob only one, so take the bigger. - Induction rule: for house i, either skip it and inherit
dp[i-1], or rob it, which rules out house i-1, givingdp[i-2] + nums[i]. Take the larger. It looks back two cells, so fill left to right. - Answer:
dp[n-1]. The definition is "only houses 0..i" and doesn't require robbing house i, so the value carries forward and never restarts.
Don't open a table yet. Recursion is the natural way to think from the top: f(i) trusts that f(i-1) and f(i-2) are already solved and only decides about the current house. Add a memo so each i is computed once:
from functools import lru_cache
def rob_memo(nums):
@lru_cache(maxsize=None) # remember the result for each i; never recompute the same i
def f(i): # f(i) = most money from houses 0..i
if i < 0: # no houses left
return 0
return max(f(i - 1), f(i - 2) + nums[i]) # skip i / rob i
return f(len(nums) - 1)
assert rob_memo([2, 7, 9, 3, 1]) == 12
assert rob_memo([2, 1, 4, 5]) == 7
assert rob_memo([5]) == 5
assert rob_memo([]) == 0
@lru_cache(maxsize=None) is a decorator, written on the line above the function. The second time f gets the same argument, it returns the stored result without running the body again; maxsize=None means the cache has no size limit. It is the memo table.
Translating the recursion into a table is a one-to-one mapping:
| Recursion + memo (big to small) | Table (small to large) |
|---|---|
Function argument i | Table index i |
if i < 0: return 0 | Base case cells filled up front |
Calls f(i-1), f(i-2) | Cells you look back at, which set the fill direction |
| The call stack computes dependencies first | The for loop guarantees dependencies are filled |
lru_cache | The dp array |
In the recursion a single i < 0 covers both edges: f(0) reads f(-2) and f(1) reads f(-1), and both return 0. The table has no cells -1 and -2, and dp[i] reads two cells back, so the base case has to give two cells directly, dp[0] and dp[1], and the loop starts at i = 2:
def rob(nums):
if len(nums) == 1: # with one house there is no dp[1], return directly
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1]) # we read two cells back, so seed two cells
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) # skip i / rob i
return dp[-1] # never restarts, answer is the last cell
assert rob([2, 7, 9, 3, 1]) == 12
assert rob([2, 1, 4, 5]) == 7
assert rob([5]) == 5
assert rob([4, 4, 4, 4]) == 8
dp[-1] is a Python negative index: it counts from the end, so it is dp[n-1].
Filling the table for nums = [2, 7, 9, 3, 1]:
| i | nums[i] | Skip: dp[i-1] | Rob: dp[i-2] + nums[i] | dp[i] |
|---|---|---|---|---|
| 0 | 2 | – | – | 2 (base) |
| 1 | 7 | – | – | 7 (base) |
| 2 | 9 | 7 | 2 + 9 = 11 | 11 |
| 3 | 3 | 11 | 7 + 3 = 10 | 11 |
| 4 | 1 | 11 | 11 + 1 = 12 | 12 |
Principle 2: dp[i] only reads the two cells to its left, which are always filled when you go left to right. Principle 3: every valid plan either robs the last house or doesn't, so it lands in "rob" or "skip".
O(n) time, O(n) space.
One more like it: Climbing Stairs (LC 70)
The input is a positive integer n. Each move climbs 1 or 2 steps. Return the number of ways to reach exactly step n. The same steps in a different order count as different ways.
For example, n = 3 → 3: 1+1+1, 1+2, 2+1.
Think first: What does dp[i] mean? The last move onto step i could come from which steps? Do those groups overlap, so do you add them or take the max? Should dp[0] be 0 or 1?
Reveal once you have an answer
dp[i] = number of ways to reach exactly step i. i is a step count and we need dp[n], so the indices run 0..n.
The last move is either one step from i-1 or two steps from i-2. The two groups have different last moves, so they don't overlap, and together they cover every way. So add them: dp[i] = dp[i-1] + dp[i-2]. Same skeleton as House Robber, with max replaced by addition.
Base cases: dp[1] = 1, a single 1-step. dp[0] = 1 means "standing on the floor and doing nothing" counts as one way. Why not 0? dp[2] should be 2 (1+1, or one 2-step), and the formula computes dp[1] + dp[0], which only works if dp[0] = 1. The problem never passes n = 0; this cell only exists so the recurrence holds from i = 2.
Fill order: it reads only the two cells to its left, so left to right. Since only two cells are read, you don't need the whole table; two variables are enough, O(n) → O(1) space:
def climb_stairs(n):
prev2, prev1 = 1, 1 # dp[0], dp[1]
for _ in range(2, n + 1): # _: the loop variable is unused
prev2, prev1 = prev1, prev1 + prev2 # one step from i-1 + two steps from i-2
return prev1
assert climb_stairs(1) == 1
assert climb_stairs(2) == 2
assert climb_stairs(3) == 3 # the example from the statement
assert climb_stairs(5) == 8
prev2, prev1 = prev1, prev1 + prev2 assigns both at once: both values on the right are computed from the old prev1 and prev2 first, then written to the left together. Split into two lines, prev2 = prev1 then prev1 = prev1 + prev2, the second line would read the already-changed prev2 and get the wrong result.
| i | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| dp[i] | 1 | 1 | 2 | 3 | 5 | 8 |
Min Cost Climbing Stairs (746) replaces "add" with "take the cheaper one plus the cost"; Tribonacci (1137) looks back three cells with three variables. Same skeleton.
Variant: houses in a circle (LC 213)
The input is an array of nonnegative integers, but the houses form a circle, so the first and last houses are neighbors too. Everything else is the same as LC 198: return the largest total.
For example, [2, 3, 2] → 3: houses 0 and 2 are neighbors around the circle and can't both be robbed, so the best is the 3 in the middle. A single house [5] → 5: a house is not its own neighbor.
Think first: Compared with LC 198 there is one extra rule: the two ends can't both be robbed. Can you split this into a few cases where each one is LC 198 on a straight row? Do those cases together miss any plan?
Reveal once you have an answer
The two ends can't both be robbed, so every valid plan skips at least one end:
- It skips the last house: that's
robon the straight rownums[0..n-2]. - It skips the first house: that's
robonnums[1..n-1].
A plan that skips both ends shows up in both cases. We want a maximum, so appearing twice does no harm; take the larger of the two runs.
def rob_circle(nums):
if len(nums) == 1: # [5][:-1] is empty, and rob doesn't accept an empty array
return nums[0]
return max(rob(nums[:-1]), rob(nums[1:])) # skip the last / skip the first
assert rob_circle([2, 3, 2]) == 3
assert rob_circle([1, 2, 3, 1]) == 4
assert rob_circle([10, 1, 1, 30]) == 31
assert rob_circle([5]) == 5 # the example from the statement
assert rob_circle([7]) == 7
nums[:-1] and nums[1:] are slices, and each returns a new list: the first drops the last element, the second drops the first. n = 1 needs its own return because both slices would be empty and rob requires a nonempty array; with n = 2 each slice has one house and rob takes its own len == 1 branch.
O(n) time; rob runs twice.
Step 1 recap
- What it solves: optimum or count problems where each step has a few choices and depends only on the previous one or two cells.
- The key line:
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]); if you look back two cells, seed two base cells. - What the variants change: Climbing Stairs replaces max with addition and sets
dp[0] = 1; the circle splits into "skip the last house" and "skip the first house", two straight rows, each solved by 198. - Easiest mistake: writing
dp[1] = nums[1].dp[1]means "only houses 0..1" and doesn't require robbing house 1, so it must bemax(nums[0], nums[1]); otherwise[5, 1]returns 1 instead of 5.
Step 1 defines dp[i] over "houses 0..i", so the answer is naturally the last cell. When the problem wants a contiguous run, the definition must become "must end at i", and the answer moves too. That's Step 2.
Step 2: Ending at i, the answer is the max over the table
Problem: Longest ascending subarray (the one from the questions above). a = [7, 2, 3, 1, 5, 8, 9, 6], return 4.
The questions are already answered: dp[i] = length of the longest ascending subarray that ends at and includes a[i]; dp[0] = 1; extend with dp[i-1] + 1 if you can, otherwise restart at 1; the answer is max(dp).
The only new thing in this step: when the definition says "must include i", the answer has to be found across the whole table. The code tracks it in best as it fills:
def longest_ascending_subarray(a):
if not a:
return 0
dp = [1] * len(a) # dp[i] = longest ascending run ending at a[i]; each number alone has length 1
best = 1
for i in range(1, len(a)):
if a[i] > a[i - 1]:
dp[i] = dp[i - 1] + 1 # extend the run that ends at a[i-1]
else:
dp[i] = 1 # can't extend, restart at i
best = max(best, dp[i])
return best # restarts happen, so take the max over the table
assert longest_ascending_subarray([7, 2, 3, 1, 5, 8, 9, 6]) == 4
assert longest_ascending_subarray([1, 2, 3, 3, 4, 4, 5]) == 3
assert longest_ascending_subarray([5]) == 1
assert longest_ascending_subarray([]) == 0
The whole dp starts at 1: every number on its own is an ascending subarray of length 1. That also fills the base case dp[0] = 1, so the loop starts at i = 1.
| i | a[i] | vs a[i-1] | Action | dp[i] | best |
|---|---|---|---|---|---|
| 0 | 7 | – | base | 1 | 1 |
| 1 | 2 | 2 < 7 | restart | 1 | 1 |
| 2 | 3 | 3 > 2 | extend | 2 | 2 |
| 3 | 1 | 1 < 3 | restart | 1 | 2 |
| 4 | 5 | 5 > 1 | extend | 2 | 2 |
| 5 | 8 | 8 > 5 | extend | 3 | 3 |
| 6 | 9 | 9 > 8 | extend | 4 | 4 |
| 7 | 6 | 6 < 9 | restart | 1 | 4 |
If the last line were return dp[-1], this example would return 1.
Why the definition must say "must include i". Switch to dp[i] = "length of the longest ascending subarray within a[0..i]" (not required to include i), keep the same rule, and try [1, 2, 0, 3]:
| i | a[i] | Rule | dp[i] | True answer |
|---|---|---|---|---|
| 0 | 1 | base | 1 | 1 |
| 1 | 2 | 2 > 1, extend | 2 | 2 |
| 2 | 0 | 0 < 2, under this definition inherit | 2 | 2 |
| 3 | 3 | 3 > 0, dp[2] + 1 | 3 | 2, wrong |
dp[2] = 2 describes [1, 2], which doesn't end at 0, so 3 can't be appended to it. The definition didn't say what the run ends with, and the rule glued the wrong pieces together.
One more like it: Maximum Subarray (LC 53, Kadane)
The input is a nonempty integer array a. Return the largest sum of a nonempty contiguous subarray. Return the sum, not the subarray.
For example, [-2, 1, -3, 4, -1, 2, 1, -5, 4] → 6, from [4, -1, 2, 1]. Even when everything is negative you must pick at least one number: [-3, -1] → -1, not the 0 of an empty subarray.
Think first: It's contiguous, so define it as "ending at a[i]", as above. A subarray ending at a[i] can come from which two places? When should you drop the run before it and restart at a[i]? Can best start at 0?
Reveal once you have an answer
dp[i] = the largest sum of a subarray that ends at and includes a[i]. There are only two sources:
- Extend the best run ending at
a[i-1]:dp[i-1] + a[i]. - Start a new run with
a[i]alone:a[i].
Take the larger: dp[i] = max(dp[i-1] + a[i], a[i]). Extending a negative sum only makes it smaller, so this is the same as "restart when dp[i-1] < 0".
Base case: dp[0] = a[0]. dp[i] only reads dp[i-1], so a single variable cur replaces the table. The answer is the table maximum, tracked in best:
def max_subarray(a):
cur = best = a[0] # cur is dp[i-1]; best must not start at 0
for x in a[1:]: # a[1:] is the slice without element 0
cur = max(cur + x, x) # extend the previous run / restart at x
best = max(best, cur)
return best
assert max_subarray([2, -1, 4, -2, 1]) == 5
assert max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6
assert max_subarray([-3, -1]) == -1 # the example from the statement
assert max_subarray([-2, -1, -3]) == -1
assert max_subarray([3]) == 3
| i | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| a[i] | 2 | -1 | 4 | -2 | 1 |
| dp[i] | 2 | max(1, -1) = 1 | max(5, 4) = 5 | max(3, -2) = 3 | max(4, 1) = 4 |
| best | 2 | 2 | 5 | 5 | 5 |
When everything is negative ([-2, -1, -3]), the answer is the largest negative number, -1. Initializing best to 0 is the same as allowing an empty subarray, and it would wrongly return 0.
O(n) time, O(1) space.
Variant: Maximum Product Subarray (LC 152)
The input is a nonempty integer array, which may contain 0 and negative numbers. Return the largest product of a nonempty contiguous subarray.
For example, [2, 3, -2, 4] → 6, from [2, 3]. [-2, 0, -1] → 0: the two negatives are separated by a 0, and you can't skip the 0 to multiply them, so the best is the 0 by itself.
Think first: Still "ending at a[i]". Is it enough to track only "the largest product ending at i", as in LC 53? When you multiply by a negative number, which value from the previous cell can become the new largest product?
Reveal once you have an answer
It isn't enough. Multiplying by a negative number turns the smallest (most negative) product into the largest. For example, the smallest product ending at 3 is -6, and times -4 that becomes 24. So track two values: the largest product hi and the smallest product lo ending at i.
For a subarray ending at x = a[i] there are only three candidates: x alone (restart), hi * x (extend the largest), lo * x (extend the smallest). The new hi is the largest of the three and the new lo is the smallest.
def max_product(a):
hi = lo = best = a[0] # largest and smallest product ending at i
for x in a[1:]:
cands = (x, hi * x, lo * x) # restart / extend the max / extend the min
hi, lo = max(cands), min(cands) # update together, both use last round's values
best = max(best, hi)
return best
assert max_product([2, 3, -2, 4]) == 6
assert max_product([-2, 3, -4]) == 24
assert max_product([-2, 0, -1]) == 0
assert max_product([-3]) == -3
The three candidates go into the tuple cands first, then max and min are taken together. Both new values use only last round's hi and lo; if you updated hi first and then computed lo, lo would use this round's new hi.
a = [-2, 3, -4]:
| i | a[i] | hi | lo | best |
|---|---|---|---|---|
| 0 | -2 | -2 | -2 | -2 |
| 1 | 3 | max(3, -6, -6) = 3 | min(3, -6, -6) = -6 | 3 |
| 2 | -4 | max(-4, -12, 24) = 24 | min(-4, -12, 24) = -12 | 24 |
Tracking only hi would lose the -6 in row 1 and end with 3 instead of 24.
Step 2 recap
- What it solves: longest or largest contiguous subarray or substring.
- The key line:
best = max(best, dp[i]). The definition is "ends at and includesa[i]", so the answer is the table maximum, notdp[-1]. - What the variants change: LC 53 replaces "extend means +1" with "extend or restart, take the larger"; LC 152 also tracks the smallest product, because a negative number flips large and small.
- Easiest mistake: starting
bestat 0. An all-negative array then returns 0; start froma[0].
So far every dp[i] looks left and the table is always filled left to right. That direction isn't fixed; it follows the definition. That's Step 3.
Step 3: Same problem, different definition, and the fill direction flips
Problem : Jump Game ( LC 55 ). nums[i] is the farthest you can jump to the right from i. (Pro lesson)
One more like it: fewest jumps (LC 45)
The input is an array nums of nonnegative integers, where nums[i] is the farthest you can jump right from i. (Pro lesson)
Variant: when the dependency order is hard to state, use recursion + memo (LC 329)
The input is a nonempty integer matrix matrix . Each move goes one cell up, down, left or right, and the next cell's value must be strictly… (Pro lesson)
Step 3 recap
What it solves : the same problem can have different dp definitions, and the fill direction follows the definition instead of always running… (Pro lesson)
Step 4: Look back at every j < i: left big part from the table, right small part computed directly
Problem : Integer Break ( LC 343 ). Cut a rope of length n at least once into integer-length pieces; maximize the product of the lengths. (Pro lesson)
One more like it: Word Break (LC 139)
The input is a nonempty string s and a list of words word_dict (called wordDict on LeetCode). (Pro lesson)
One more like it: Decode Ways (LC 91)
Digits map to letters as '1' → A, '2' → B, …, '26' → Z. The input is a nonempty digit string s . (Pro lesson)
Variant: j is "the previous element", not a cut (LC 300)
The input is an integer array nums . Return the length of the longest strictly increasing subsequence. (Pro lesson)
Variant: counting problems must not double count (LC 940)
The input is a string s of lowercase letters. Return how many distinct nonempty subsequences it has. (Pro lesson)
Step 4 recap
What it solves : problems that look back at every shorter prefix: cutting, splitting into dictionary words, subsequences. (Pro lesson)
Step 5: Two indices dp[i][j], look back left, up, and up-left
Problem : Longest Common Subsequence ( LC 1143 ). a = "abcde" , b = "ace" , return 3. The input is two strings. (Pro lesson)
Contrast: Longest Common Substring (must be contiguous)
The input is two strings a and b . Return the length of their longest common substring . A substring must be contiguous; (Pro lesson)
One more like it: Edit Distance (LC 72)
The input is two strings a and b , either of which may be empty. (Pro lesson)
One more like it: grid paths (LC 62)
The input is positive integers m and n, a grid with m rows and n columns. (Pro lesson)
Variant: with (i, j) as the bottom-right corner (LC 221)
The input is a matrix of "0" and "1" (each element is a string, not an integer). (Pro lesson)
Step 5 recap
What it solves : comparing, matching or editing two strings, or walking a grid and finding squares. (Pro lesson)
Step 6: Knapsack, where loop order is the meaning
Problem : Partition Equal Subset Sum ( LC 416 ). (Pro lesson)
Turn it around: unbounded knapsack (LC 322)
The input is an array coins of distinct positive denominations and a nonnegative integer amount . (Pro lesson)
One step further: which loop is outside (LC 518 vs LC 377)
Both problems give a set of distinct positive integers, each usable any number of times, and ask how many ways there are to reach a target s… (Pro lesson)
Step 6 recap
What it solves : a capacity or amount limit and items to choose from; can you make it, how few, how many ways. (Pro lesson)
Advanced: when one cell can't hold enough, add a dimension
So far the state has been a position (plus a capacity, at most). (Pro lesson)
State machine: stock with cooldown (LC 309)
The input is the daily stock prices prices . You may buy and sell many times, but you hold at most one share, so you must sell before buying… (Pro lesson)
Interval DP: counting palindromic substrings (LC 647)
The input is a string s . Return the number of palindromic substrings. A substring must be contiguous; (Pro lesson)
Games: Stone Game (LC 877)
The input is an even number of piles piles , each a positive integer, with an odd total, so there are no ties. (Pro lesson)
Interval DP: enumerate the last move (LC 312)
The input is an array nums of nonnegative integers, each the value of a balloon, and you must burst every balloon. (Pro lesson)
Advanced recap
What it solves : information that doesn't fit in one cell (holding a stock or not, which range, whose turn) goes into the state. (Pro lesson)
The six steps together
Step dp definition Looks back at Fill order Answer Step 1 only 0..i i-1, i-2 left to right last cell Step 2 ending at i, must include i i-1… (Pro lesson)
One Problem, Many Disguises
Disguise (how the problem says it) Translate to the core problem Step Climb 1 or 2 steps at a time, how many ways (70) dp[i] = dp[i-1] + dp[… (Pro lesson)
Common Pitfalls
i is a length but you allocate only n cells. dp = [False] * len(s) with dp[i] meaning the prefix of length i leaves no cell for length n, or… (Pro lesson)
Say It in the Interview
State what dp[i] means first, then the base case, the transition and where the answer is, and finish with the complexity: I'll use dynamic p… (Pro lesson)
Practice problems, in order
- 70. Climbing Stairs · Easy
- 198. House Robber · Medium
- 53. Maximum Subarray · Medium
- 62. Unique Paths · Medium
- 300. Longest Increasing Subsequence · Medium
- 1143. Longest Common Subsequence · Medium
- 322. Coin Change · Medium
- 416. Partition Equal Subset Sum · Medium
- 518. Coin Change II · Medium