Coding Interview Patterns Module 24 – Dynamic programming: foundations
Designing DP states and transitions
Design a DP state as the least you must know to decide the rest, then write its transitions, base cases and answer, and test it against a brute force.
What you will learn
- Define a DP state as the least information the rest of the problem needs
- Write the transitions, base cases and answer of a DP in words before coding it
- Classify a DP question as counting, optimisation or feasibility
- Test a state design against a brute force to catch missing or surplus information
- Rebuild the choices behind a DP's answer from its table
Before you start
What you’ll learn · Before you start
What you will learn
- Define a DP state as the least information the rest of the problem needs
- Write the transitions, base cases and answer of a DP in words before coding it
- Classify a DP question as counting, optimisation or feasibility
- Test a state design against a brute force to catch missing or surplus information
- Rebuild the choices behind a DP's answer from its table
Before you start
On this page
Designing DP states means deciding what one entry of a dynamic-programming table stands for. A good state is the least information about the choices already made that the rest of the problem needs. The test is simple: two partial solutions that reach the same state must allow exactly the same ways to finish, so the table can keep the better of them, or their count. Around the state you write the transitions (the choices a state has and where each leads), the base cases (states with nothing left to decide) and the answer (the state the problem starts from). A wrong state returns wrong numbers and raises no error, so every design is checked against a brute force, as in the stress-testing lesson.
This lesson works through designing DP states for one original problem: its brute force first, then the state, its transitions and a table filled by hand.
A revision plan with rest days
The problem
There are n days before an exam. Studying on day i is worth values[i] points, and the rule is that you may study
on at most k days in a row: after k study days in a row the next day must be a rest day. Return the largest total
a plan can earn. Limits: 0 ≤ n ≤ 100,000, 1 ≤ k ≤ 20 and 0 ≤ values[i] ≤ 10,000.
[5, 3, 8, 6, 2]with k = 2 gives 19: study on days 0, 2 and 3, rest on days 1 and 4.- The same days give 15 with k = 1 (days 0, 2 and 4) and 24 with k = 5 (every day).
- No days at all give 0.
The brute force: every plan
A plan says, for each day, study or rest, so n days have 2ⁿ plans. Try each one, drop those with more than k study days in a row, and keep the best total:
from itertools import product
def best_plan_brute(values, k):
"""Tries every study-or-rest plan and keeps the best one with at most k study days in a row."""
best, best_days, checked = 0, [], 0
for plan in product((False, True), repeat=len(values)): # True: study that day
checked += 1
run = longest = 0
for study in plan:
run = run + 1 if study else 0
longest = max(longest, run)
if longest <= k:
total = sum(v for v, study in zip(values, plan) if study)
if total > best:
best, best_days = total, [day for day, study in enumerate(plan) if study]
return best, best_days, checked
for values, k in [([5, 3, 8, 6, 2], 2), ([5, 3, 8, 6, 2], 1), ([5, 3, 8, 6, 2], 5), ([], 2)]:
best, days, checked = best_plan_brute(values, k)
studied = f"days {', '.join(map(str, days))}" if days else "no day"
plans = "plan" if checked == 1 else "plans"
print(f"{values}, k = {k}: best {best}, studying on {studied} ({checked} {plans} checked)")
for n in (8, 12, 16):
*_, checked = best_plan_brute([1] * n, 2)
print(f"{n} days: {checked:,} plans checked") Output
[5, 3, 8, 6, 2], k = 2: best 19, studying on days 0, 2, 3 (32 plans checked) [5, 3, 8, 6, 2], k = 1: best 15, studying on days 0, 2, 4 (32 plans checked) [5, 3, 8, 6, 2], k = 5: best 24, studying on days 0, 1, 2, 3, 4 (32 plans checked) [], k = 2: best 0, studying on no day (1 plan checked) 8 days: 256 plans checked 12 days: 4,096 plans checked 16 days: 65,536 plans checked
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 plans_brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The answers are right, so the brute force will judge faster versions, but each extra day doubles the work: 65,536 plans for 16 days, and for 100,000 days a number of plans with 30,103 digits.
What the rest of the plan needs to know
Stand at the start of day i with the earlier days already planned, and ask which of those decisions can change what is allowed from now on. The points they earned cannot: they are banked. Which days they were cannot either. Only one fact matters, the streak: how many study days in a row end just before day i. If the streak is k, day i must be a rest day; otherwise both choices are open, and each choice sets the next streak. Two partial plans with the same day and streak can be finished in exactly the same ways, so the table only needs the better of them.
What the rest of the plan needs to know, for k = 2
Text description of the diagram
The diagram has three boxes, one for each value of the streak, the number of study days in a row just before the day being decided, when at most two study days in a row are allowed.
- Streak 0: the day before was a rest day, or there was no day before. Studying today adds the day's value and leads to streak 1; resting keeps the streak at 0.
- Streak 1: one study day just before. Studying adds the day's value and leads to streak 2; resting leads back to streak 0.
- Streak 2: two study days in a row. Today must be a rest day, so the only arrow leads back to streak 0.
Everything else about the days already planned, which days they were and what they earned, makes no difference to which plans for the remaining days are allowed. That is why the state is the day and the streak, and nothing more.
The state and its transitions, in words
- State: best(i, s) is the most points days i to n − 1 can still add when the s days just before day i were all study days, for 0 ≤ s ≤ k.
- Transitions: a rest on day i leads to best(i + 1, 0). A study day, allowed only when s < k, earns
values[i]and leads to best(i + 1, s + 1). best(i, s) is the larger of the allowed choices. - Base cases: best(n, s) = 0 for every s, because no days are left to earn anything.
- Answer: best(0, 0), since no study day comes before day 0.
There are (n + 1)(k + 1) states, 2,100,021 at the largest limits, each with at most two choices, so the table costs O(n·k) time. Row i reads only row i + 1, so it can be filled from the last day backwards.
The table, filled by hand
The table for [5, 3, 8, 6, 2] and k = 2, from the last row up:
| Day i | values[i] |
s = 0 | s = 1 | s = 2 |
|---|---|---|---|---|
| 5 (no days left) | — | 0 | 0 | 0 |
| 4 | 2 | 2 | 2 | 0 |
| 3 | 6 | 8 | 6 | 2 |
| 2 | 8 | 14 | 10 | 8 |
| 1 | 3 | 14 | 14 | 14 |
| 0 | 5 | 19 | 19 | 14 |
Take day 2 with s = 0: studying earns 8 plus best(3, 1) = 6, which makes 14, and resting gives best(3, 0) = 8, so the entry is 14. At s = 2 only a rest is allowed. The answer best(0, 0) = 19 matches the brute force, and following the choice behind each maximum gives the plan: study on days 0, 2 and 3.
Counting, optimisation and feasibility
The same state answers three kinds of question, and only the way choices combine changes:
- Optimisation keeps the larger or smaller choice, as above, and its empty rest of a plan is worth 0.
- Counting adds the counts of the choices, so the empty rest counts as 1 way, usually modulo a prime.
- Feasibility asks whether any choice works, so the empty rest counts as true.
Exercise
Exercise · Easy · Python
Count the rows of festival lights with no three alike
A row of n festival bulbs is to be strung, and each bulb can be any of c colours. The only rule: no three bulbs next to each other may have the same colour. Write light_rows(n, c), the number of different rows, modulo 1,000,000,007, for 1 ≤ n ≤ 100,000 and 1 ≤ c ≤ 100,000.
light_rows(1, 3) # 3
light_rows(3, 3) # 24: all 27 rows except red-red-red, green-green-green and yellow-yellow-yellow
light_rows(4, 2) # 10
Design the state before you write any code. Say in words what the rest of the row needs to know about the bulbs already placed, check the words against a few rows written out by hand, and only then write the transitions, the base case and the answer.
The sample tests compare your function with a listing of every row for small n and c, try one colour (where a row of three or more bulbs is impossible), and run n = c = 100,000, where the counts must come back reduced.
Starter code · lights.py
MOD = 1_000_000_007
def light_rows(n, c):
"""The number of rows of n bulbs in c colours with no three neighbours alike, modulo MOD."""
# Replace this line with your code.
return 0 The sample tests · test_lights.py
from itertools import product
from lights import light_rows
MOD = 1_000_000_007
def listed(n, c):
"""The answer the tests trust for small rows: every row of n bulbs in c colours, written out and checked."""
return sum(
all(not (row[i] == row[i + 1] == row[i + 2]) for i in range(n - 2))
for row in product(range(c), repeat=n)
)
def test_the_examples():
"""the examples of the prompt"""
assert light_rows(1, 3) == 3
assert light_rows(3, 3) == 24
assert light_rows(4, 2) == 10
def test_small_rows_against_a_listing():
"""n = 1 to 7 and c = 2 to 4, each compared with every row listed one by one"""
for n in range(1, 8):
for c in range(2, 5):
assert light_rows(n, c) == listed(n, c), f"light_rows({n}, {c})"
def test_one_colour():
"""with one colour, one or two bulbs make one row, and three or more make none"""
assert [light_rows(n, 1) for n in (1, 2, 3, 10)] == [1, 1, 0, 0]
def test_counts_above_the_modulus():
"""n = 40 and c = 3: the count is far above 1,000,000,007, so it must come back reduced"""
assert light_rows(40, 3) == 340_831_928_248_172_544 % MOD
def test_a_long_row_of_many_colours():
"""n = c = 100,000: a loop over the positions, with the counts reduced as it goes"""
assert light_rows(100_000, 100_000) == 948_181_920 A hint
The next bulb does not care which colours came before it, only whether it may repeat the last colour. When is that allowed? Two numbers per position are enough: the rows that end with two bulbs of the same colour, and the rows that do not.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Two wrong states and one right one, tested against the brute force Pro: opens with a Pro, Premium or Ultimate pass
A state that lacks information returns wrong numbers without any error. Here two tempting designs run against the brute force until they fail, a design with too much information is measured, and an empty input breaks a table.
In this part:
- Too little state, reproduced: two designs that forget the streak
- Too much state, measured: the whole plan as the key
- A base-case bug that only an empty input shows
- A checklist for any state you design
3 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
The revision plan proved, in O(k) memory, in five languages Pro: opens with a Pro, Premium or Ultimate pass
The table becomes a loop of a few lines. This part proves the loop right, cuts its memory to two rows of k + 1 numbers, and runs it in five languages on the same six inputs.
In this part:
- Why the table is right, by induction from the last day
- Two rows of k + 1 numbers instead of a table
- The code in five languages
- The cost, state by state
2 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
One state, three questions: counting, feasibility and the plan itself Pro: opens with a Pro, Premium or Ultimate pass
A state that is right for the best total is right for other questions about the same plans. Here it counts the plans, decides whether fixed study and rest days can be kept, and rebuilds the best plan from the full table.
In this part:
- Counting the valid plans modulo 1,000,000,007
- Required and blocked days, as a yes-or-no table
- Rebuilding the best plan from the table
- Practice: five sketches and the state each needs
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is a state in dynamic programming, and how do you check that a state carries enough information?
A state is one entry of the table: a few values, such as a position and a count, that summarise the choices made so far. It must hold the least information the rest of the problem needs. The check is to take two different partial solutions that reach the same state and ask whether they allow exactly the same ways to finish. If they do, the table may keep only the better one, or only their count. If a completion is allowed after one and forbidden after the other, the state is missing what tells them apart. In the revision plan of the lesson, the day alone fails this test, because a streak of k study days and a rest the day before look the same; the day and the streak pass it. Then compare the design with a brute force on small inputs.
-
Mid to senior
Your DP for a scheduling problem passes the examples, but on a hidden test it reports a total higher than any valid schedule can earn. You suspect the state. How do you find what the state is missing, and how do you fix it without guessing?
-
Mid to senior
A memoised solution of the revision plan keys its cache on the day and a bitmask of the last k decisions, with k up to 20 and n up to 100,000, and it runs out of memory. How many states can it create, what smaller state would you use, and why does it lose nothing?
-
Senior to staff
Besides the best total of the revision plan, return how many different plans reach that total, modulo 1,000,000,007. What does each state store, how do ties combine, and what does it cost?
Key takeaways
- A DP state is the least the rest of the problem needs to know: for the revision plan, the day and the streak.
- Two partial solutions in the same state must allow the same completions.
- Write the state, transitions, base cases and answer in words first, and fill a small table by hand.
- Optimisation, counting and feasibility share a state; only how choices combine and the base case change.
References
- NIST Dictionary of Algorithms and Data Structures: dynamic programming (National Institute of Standards and Technology (NIST))
- functools.cache (The Python Standard Library) (Python Software Foundation)
- itertools.product (The Python Standard Library) (Python Software Foundation)
- The Java Language Specification, Java SE 25: 4.2.1 Integral types and values (Oracle)
- The Go Programming Language Specification: Min and max (The Go Authors)
- The Go Programming Language Specification: Numeric types (The Go Authors)
- ECMAScript 2026 Language Specification: Number.MAX_SAFE_INTEGER (Ecma International)
- AddressSanitizer (Clang documentation) (The LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress