Data Structures & Algorithms (DSA) Module 14 – Dynamic programming
1-D dynamic programming: take or skip
Solve sequence problems with one table entry per position: take-or-skip choices traced on a street, two variables instead of a table, and the choice rebuilt.
What you will learn
- Solve linear DP problems with take-or-skip transitions
- Optimise the memory of a linear DP from O(n) to O(1)
- Rebuild the chosen elements from the table by walking back
- Recognise the same one-dimensional shape in counting and scheduling problems
Before you start
What you’ll learn · Before you start
What you will learn
- Solve linear DP problems with take-or-skip transitions
- Optimise the memory of a linear DP from O(n) to O(1)
- Rebuild the chosen elements from the table by walking back
- Recognise the same one-dimensional shape in counting and scheduling problems
Before you start
On this page
One-dimensional dynamic programming (1-D DP) solves a problem over a sequence with one table entry per position: best[i] is the answer for the first i items, and each entry is worked out from a few entries just before it, so one pass over the sequence fills the table in O(n) time. The most common shape is take or skip. On a street where two neighbouring houses may not both be visited, house i is either skipped, which leaves best[i − 1], or taken, which adds its gift to best[i − 2], the best that stops short of its neighbour. So best[i] is the larger of best[i − 1] and best[i − 2] + gift(i). Because each entry looks back only two places, two variables can replace the table, and walking back through the table tells you which houses were chosen. This lesson traces the recurrence on a street of eight houses, runs it, cuts its memory to two numbers and rebuilds the choice.
A street of donations
A volunteer collects donations along a street of eight houses. The housing society asks that no two neighbouring houses are visited on the same day, and the gifts on offer are 40, 10, 60, 50, 20, 90, 30 and 70. Which houses should the volunteer visit to collect the most?
The obvious plans fall short. Every other house gives 40 + 60 + 20 + 30 = 150 from the odd houses, or 220 from the even ones. Trying every allowed set works for eight houses, which allow only 55 sets, but the count grows like the Fibonacci numbers, by about 1.6 for each extra house: a street of 100 houses allows about 9.3 × 10²⁰ of them.
Skip or take: the recurrence
Write the DP in its four parts. The state is best[i], the most that the first i houses can give. The transition looks at house i, the last of them, which is either skipped or taken:
Every house is skipped or taken
Text description of the diagram
The diagram shows the one decision that 1-D DP makes for each house, from top to bottom.
- best[i] is the most that the first i houses can give.
- Skipping house i keeps best[i − 1], the most from the houses before it.
- Taking house i adds its gift to best[i − 2], because house i − 1, its neighbour, must then stay unvisited.
- best[i] is the larger of the two.
For house 8 of the lesson's street, skipping it keeps 190 and taking it gives 70 + 190 = 260, so best[8] = 260.
The base cases are best[0] = 0 for an empty street and best[1] = gift(1), which the rule also gives if best[−1] is read as 0. The answer is best[n]. Filling i from 1 up to n keeps every entry after the two it reads.
The table, traced on the street
"""The most donations along a street when two neighbouring houses may not both be visited."""
import random
from itertools import product
donations = [40, 10, 60, 50, 20, 90, 30, 70]
def best_table(d):
"""best[i]: the most that the first i houses can give; best[0] = 0, a street with no houses."""
best = [0] * (len(d) + 1)
for i in range(1, len(d) + 1):
skip = best[i - 1]
take = (best[i - 2] if i >= 2 else 0) + d[i - 1]
best[i] = max(skip, take)
return best
def chosen_houses(d, best):
"""Walk back from the last house: an entry equal to the one before it means the house was skipped."""
houses, i = [], len(d)
while i >= 1:
if best[i] == best[i - 1]:
i -= 1
else:
houses.append(i)
i -= 2
return houses[::-1]
def best_two_numbers(d):
before, last = 0, 0 # best[i - 2] and best[i - 1]
for x in d:
before, last = last, max(last, before + x)
return last
def every_selection(d):
best = 0
for picks in product((0, 1), repeat=len(d)):
if not any(picks[i] and picks[i + 1] for i in range(len(d) - 1)):
best = max(best, sum(x for x, p in zip(d, picks) if p))
return best
best = best_table(donations)
print(f"{'house':>5} {'gift':>5} {'skip':>5} {'take':>5} {'best':>5}")
for i in range(1, len(donations) + 1):
take = (best[i - 2] if i >= 2 else 0) + donations[i - 1]
print(f"{i:>5} {donations[i - 1]:>5} {best[i - 1]:>5} {take:>5} {best[i]:>5}")
houses = chosen_houses(donations, best)
print(f"houses visited: {houses}, total {sum(donations[h - 1] for h in houses)}")
print(f"two numbers instead of a table: {best_two_numbers(donations)}")
print(f"every allowed selection tried: {every_selection(donations)}")
rng = random.Random(5)
streets = [[rng.randint(0, 99) for _ in range(rng.randint(0, 12))] for _ in range(2_000)]
agree = all(best_table(s)[-1] == best_two_numbers(s) == every_selection(s) for s in streets)
print(f"2,000 random streets of 0 to 12 houses, table = two numbers = every selection: {agree}") Output
house gift skip take best
1 40 0 40 40
2 10 40 10 40
3 60 40 100 100
4 50 100 90 100
5 20 100 120 120
6 90 120 190 190
7 30 190 150 190
8 70 190 260 260
houses visited: [1, 3, 6, 8], total 260
two numbers instead of a table: 260
every allowed selection tried: 260
2,000 random streets of 0 to 12 houses, table = two numbers = every selection: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 street.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
Read a few rows. At house 3, taking it gives 60 + best[1] = 100, more than the 40 of skipping it. At house 4, taking gives only 50 + 40 = 90, so best[4] stays 100. House 6 is worth taking, 90 + 100 = 190, and house 8 lifts the total to 70 + 190 = 260.
The table stores totals, not houses, but the houses can be read back from it. Start at the last house. If best[i] equals best[i − 1], house i can be skipped without loss, so move to i − 1. Otherwise house i was taken: record it and jump to i − 2, past its neighbour. Here the walk takes house 8, jumps to 6 and takes it, skips 4, takes 3, jumps to 1 and takes it: houses 1, 3, 6 and 8 for 260. The run checks that answer against all 55 allowed sets, and the table against the same search on 2,000 random streets.
Two numbers instead of a table
Each entry reads only the two entries before it, so once best[i] is known, best[i − 2] is never read again. Keeping
two variables, before and last, cuts the memory from O(n) to O(1) and still gives 260. The price is the walk
back, which needs every entry: keep the table, or one bit per house that records the choice, when the houses matter,
and the two numbers when only the total does.
The same shape in other problems
Many problems are this table with a different look-back:
- Digit decodings. Counting the ways to read a string of digits as letters adds the two entries before each position, but only when those digits form a letter.
- A circular street. When the first and last houses are neighbours too, the answer is the better of two straight streets.
- Weighted interval scheduling. For paid gigs with start and end times, taking a gig looks back to the last gig that ends before it starts.
- Rest days. If every working day needs k rest days after it, taking a day looks back k + 1 places, which the exercise asks you to write.
Exercise
Exercise · Medium · Python
Plan working days with k rest days after each
A freelance sound engineer is offered one paid job on each day of a festival season: values[d] is what day d would pay, and every value is at least 0. After any working day they must rest for at least k days before working again, so with k = 2 they could work days 0, 3 and 6, but not days 0 and 2.
Write best_days(values, k), which returns the largest total they can earn. The starter code solves only k = 1, the street of houses from the lesson. Change its transition so that taking day i looks back far enough for any k ≥ 0, and keep it O(n) in time.
The sample tests check k = 1, k = 0 (no rest needed at all), k = 2, a k longer than the season, an empty season, and 300 random seasons against a slow function that tries every set of days.
Starter code · rest_days.py
def best_days(values, k):
"""The largest total of values[d] over working days d, with at least k rest days after each working day."""
# This solves k = 1 only: taking day i adds to the best up to two days earlier.
# Change how far back "take" looks so that it works for any k >= 0.
before, last = 0, 0
for v in values:
before, last = last, max(last, before + v)
return last The sample tests · test_rest_days.py
import random
from itertools import combinations
from rest_days import best_days
def slow_best(values, k):
"""Every set of days, kept when consecutive working days are at least k + 1 apart."""
best = 0
for r in range(len(values) + 1):
for days in combinations(range(len(values)), r):
if all(b - a > k for a, b in zip(days, days[1:])):
best = max(best, sum(values[d] for d in days))
return best
def test_one_rest_day():
"""k = 1 is the street of houses: no two neighbouring days"""
assert best_days([40, 10, 60, 50, 20, 90, 30, 70], 1) == 260
def test_no_rest_needed():
"""k = 0: every day can be a working day"""
assert best_days([3, 1, 4, 1, 5], 0) == 14
def test_two_rest_days():
"""k = 2: days 0, 3 and 6; and two of the three 9s, since 9s two days apart are too close"""
assert best_days([5, 1, 1, 5, 1, 1, 5], 2) == 15
assert best_days([9, 1, 9, 1, 9], 2) == 18
def test_rest_longer_than_the_season():
"""a k longer than the season leaves room for one working day: the best single day"""
assert best_days([4, 8, 2, 6], 10) == 8
def test_empty_season():
"""no days, no earnings"""
assert best_days([], 3) == 0
def test_against_every_set_of_days():
"""300 random seasons of up to 10 days and k from 0 to 4"""
rng = random.Random(12)
for _ in range(300):
values = [rng.randint(0, 20) for _ in range(rng.randint(0, 10))]
k = rng.randint(0, 4)
assert best_days(values, k) == slow_best(values, k), (values, k) A hint
Use a table best where best[i] is the most the first i days can earn, with best[0] = 0. Skipping day i gives best[i - 1]; working it gives values[i - 1] plus the best of the days that end at least k days before it, which is best[i - k - 1], or 0 when that index is below 0.
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
3 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.
Why take-or-skip is right, and four bugs that break it Pro: opens with a Pro, Premium or Ultimate pass
The recurrence has a short proof, and most wrong answers come from one of four slips. This part proves the table correct and runs each slip against it until it fails, with the smallest street that shows the failure.
In this part:
- The proof: a best selection skips the last house or takes it
- Four plausible bugs, each with the smallest street that breaks it
- Edge cases worth a test
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).
Digit decodings, a circular street and paid gigs, solved and checked Pro: opens with a Pro, Premium or Ultimate pass
Three problems that look different are the same one-dimensional table with a different look-back. Each is solved here and checked against a brute force on hundreds or thousands of random inputs.
In this part:
- Counting the readings of a digit string, zeros included
- A circular street as two straight streets
- Weighted interval scheduling in O(n log n), with the gigs rebuilt
4 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 street in five languages, and the 32-bit total that breaks it Pro: opens with a Pro, Premium or Ultimate pass
The table and the walk back are a few lines in every interview language, and all five versions print the same lines. One extra run in Java shows what a 32-bit total does on a long street.
In this part:
- The code in Python, JavaScript, Java, C++ and Go
- A long street: totals past 2³¹ − 1, reproduced in Java
- Practice set
- Key takeaways of these parts
2 runnable examples (Build and run all three, C++, Go, Java, JavaScript, Python) · about 2 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 · Warm-up
In the take-or-skip recurrence for a street of houses, why does taking house i add best[i − 2] rather than best[i − 1], and what are the base cases?
Taking house i rules out house i − 1, its neighbour, so the rest of the selection must come from the first i − 2 houses, and the most they can give is best[i − 2]. best[i − 1] may already include house i − 1, so adding house i to it could visit two neighbours: on the street 1, 1 that slip returns 2 instead of 1. Skipping house i leaves the first i − 1 houses free, which is best[i − 1]. The base cases are best[0] = 0, a street with no houses, and best[1] = gift(1), which the rule also gives if best[−1] counts as 0. In 0-indexed code the second entry is the larger of the first two gifts, not the second gift: the second gift alone is wrong whenever the first is larger, as on the street 2, 1.
-
Fresher to mid
A teammate's solution visits every other house, starting from whichever of the first two houses gives more. It passes all five sample tests. Find the smallest street that breaks it, explain why, and replace it with an O(n) solution. What memory does yours need to also return the houses?
-
Fresher to mid
Your function counts the ways to read a string of digits as letters, with 1 = A up to 26 = Z. It is right on 12 and 226 but wrong on 10, 100 and 2101. What do you fix in the transition and the base cases, and how do you test the fix?
-
Mid to senior
A booking service picks non-overlapping paid gigs with the largest total, from a table over gigs sorted by end time, and finds each gig's last compatible gig by scanning backwards. It is instant on a week of short gigs but takes minutes on a year of long, overlapping ones. Explain why with numbers, fix it and return the gigs.
Key takeaways
- 1-D DP keeps one entry per position, best[i] for the first i items, and fills it in one pass.
- Take or skip: best[i] is the larger of best[i − 1] and best[i − 2] + gift(i), with best[0] = 0.
- On the lesson’s street the table finds 260 from houses 1, 3, 6 and 8, where every other house gives at most 220.
- Two variables are enough for the total; the walk back to the chosen items needs the table or a record of choices.
References
- The theory of dynamic programming (Bulletin of the American Mathematical Society 60(6)) (American Mathematical Society)
- bisect: array bisection algorithm (Python Software Foundation)
- NIST Dictionary of Algorithms and Data Structures: dynamic programming (National Institute of Standards and Technology (NIST))
- NIST Dictionary of Algorithms and Data Structures: Fibonacci number (National Institute of Standards and Technology (NIST))
- The Java Language Specification, Java SE 25: integer operations (15.18.2, overflow) (Oracle)
- Number.MAX_SAFE_INTEGER (JavaScript reference) (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress