Coding Interview Patterns Module 26 – Dynamic programming: advanced patterns
State-machine DP: buy, sell, cool down
Model buying, selling and a rest day as states of a machine, then keep the top profit of each state per day: the brute force, the idea and a dry run.
What you will learn
- Model holding, not holding and cooling down as the states of a machine
- Draw the state diagram before coding, with one arrow per allowed action
- Write the day-by-day update from yesterday's values and prove it correct
- Handle a fee per sale and at most k transactions, and decide when k stops mattering
- Reproduce the common update-order and initialisation bugs against a brute force
Before you start
What you’ll learn · Before you start
What you will learn
- Model holding, not holding and cooling down as the states of a machine
- Draw the state diagram before coding, with one arrow per allowed action
- Write the day-by-day update from yesterday's values and prove it correct
- Handle a fee per sale and at most k transactions, and decide when k stops mattering
- Reproduce the common update-order and initialisation bugs against a brute force
Before you start
On this page
State-machine DP solves problems where each step, such as a day, moves you between a few named situations, the states, and the rules say which moves are allowed. Instead of remembering the whole history, keep one number per state: the best result of any plan that ends the current step in that state. Each new step computes every state’s number from the previous step’s numbers, through the allowed moves. For buying and selling with a rest day, the states are holding a crate, having sold today, and resting; the moves are buy, sell, wait and the forced day of rest. The whole run costs O(n × states) time and O(states) memory.
The problem: a trading game with a rest day
The problem
In a trading game, a crate of a made-up spice has price prices[d] on day d. Each day the trader may buy a crate
(holding none), sell it (holding one) or wait, and holds at most one crate. Every sale pays a fee, and the day
after a sale is a day of rest, with no buying. Write max_profit(prices, fee), the best total profit. Limits:
1 ≤ n ≤ 10⁵ days, prices from 1 to 10⁴, 0 ≤ fee ≤ 10⁴.
The prices are invented for the game: this is an exercise in algorithms, not advice about any market. Examples:
[5, 3, 6, 8, 2, 7, 4, 9]with a fee of 1 gives 8, for example by buying at 3, selling at 8, resting on the day of 2, then buying at 4 and selling at 9.- The same prices with no fee give 10.
- Falling prices,
[9, 7, 4, 2], give 0: never trading is allowed.
The brute force, and what it costs
Try every plan: on each day, wait, or take the one action the rules allow. Without the rest day and the fee, the answer would be the sum of every rise; with them, the choices interact, which is why the search is the reference.
"""Every plan of the trading game, tried one day at a time: wait, or buy, or sell."""
import random
def best_by_search(prices, fee, cooldown=1):
"""The best profit over every plan, and how many complete plans the search tried."""
best, plans = 0, 0
def day(d, holding, rest_left, profit):
nonlocal best, plans
if d == len(prices):
plans += 1
if not holding: # a crate still held at the end earns nothing back
best = max(best, profit)
return
day(d + 1, holding, max(0, rest_left - 1), profit) # wait
if holding:
day(d + 1, False, cooldown, profit + prices[d] - fee) # sell, then rest
elif rest_left == 0:
day(d + 1, True, 0, profit - prices[d]) # buy
day(0, False, 0, 0)
return best, plans
print("prices 5 3 6 8 2 7 4 9, fee 1 ->", best_by_search([5, 3, 6, 8, 2, 7, 4, 9], 1))
print("same prices, no fee ->", best_by_search([5, 3, 6, 8, 2, 7, 4, 9], 0))
print("falling prices 9 7 4 2, fee 1 ->", best_by_search([9, 7, 4, 2], 1))
rng = random.Random(8)
for n in (12, 16, 20, 24):
prices = [rng.randint(1, 50) for _ in range(n)]
best, plans = best_by_search(prices, 1)
print(f"{n} days: {plans:,} plans tried") Output
prices 5 3 6 8 2 7 4 9, fee 1 -> (8, 114) same prices, no fee -> (10, 114) falling prices 9 7 4 2, fee 1 -> (0, 12) 12 days: 1,081 plans tried 16 days: 10,252 plans tried 20 days: 97,229 plans tried 24 days: 922,111 plans tried
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 every_plan.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 eight days have 114 complete plans, and each extra day multiplies the count by about 1.75: 922,111 plans for 24 days. At 10⁵ days the count has more than 24,000 digits.
What to notice
Two plans that reach the end of a day in the same state have the same future: what can still happen depends only on whether a crate is held and whether today was a sale, not on how they got there. So of all the plans that end day d in a given state, only the most profitable one matters, and the search collapses to three numbers per day.
The key idea: one best profit per state
The trading day as a state machine
Text description of the diagram
The diagram has three states. "Rest" means holding nothing and being free to buy. "Hold" means holding one crate. "Sold" means the crate was sold today.
From rest, the trader may wait, which keeps the state at rest, or buy, which moves to hold and subtracts the day's price from the profit. From hold, the trader may wait, which keeps the state at hold, or sell, which moves to sold and adds the day's price minus the fee. From sold there is only one arrow: the next day is a day of rest, which moves back to rest. There is no arrow from sold to hold, which is how the cooldown is enforced. Each state keeps the best profit of any plan that ends the day in it.
Call the three numbers hold, sold and rest. Before day 0, rest is 0 and the other two are unreachable. On
day d, with price p, each arrow of the diagram becomes one term:
- hold: the best of keeping yesterday’s crate (
hold) and buying today from rest (rest − p). - sold: selling yesterday’s crate today:
hold + p − fee. - rest: the best of resting again (
rest) and the forced rest after yesterday’s sale (sold).
Every right-hand side uses yesterday’s values, so each day allows exactly one move. The answer is the better of
sold and rest on the last day; a crate still held is worth nothing. A limit of at most k sales adds one thing to
the state, the number of sales so far.
A dry run on the example
Prices [5, 3, 6, 8, 2, 7, 4, 9], fee 1; “none” marks a state no plan can be in yet.
| Day | Price | hold | sold | rest |
|---|---|---|---|---|
| 0 | 5 | −5 | none | 0 |
| 1 | 3 | −3 | −3 | 0 |
| 2 | 6 | −3 | 2 | 0 |
| 3 | 8 | −3 | 4 | 2 |
| 4 | 2 | 0 | −2 | 4 |
| 5 | 7 | 0 | 6 | 4 |
| 6 | 4 | 0 | 3 | 6 |
| 7 | 9 | 0 | 8 | 6 |
The last row gives 8. On day 4 hold becomes 0 by buying at 2 from the rest of 2, the profit of selling at 6.
Exercise
Exercise · Medium · Python, JavaScript
The best profit with at most k sales and a fee
A new season of the trading game changes two rules. There are no rest days: a crate may be bought on the day after a sale. But the season allows at most k sales in all, and every sale still pays fee. The trader holds at most one crate at a time and may buy, sell or wait once a day. The prices are made up for the game.
Write max_profit_k(prices, k, fee), the best total profit, for 1 ≤ n ≤ 20,000 days, 0 ≤ k ≤ 20,000, prices from 1 to 10,000 and 0 ≤ fee ≤ 10,000. Never trading is allowed, so the answer is at least 0.
max_profit_k([5, 3, 6, 8, 2, 7, 4, 9], 0, 0) # 0: no sales allowed
max_profit_k([5, 3, 6, 8, 2, 7, 4, 9], 1, 0) # 7: buy at 2, sell at 9
max_profit_k([5, 3, 6, 8, 2, 7, 4, 9], 2, 1) # 10
max_profit_k([9, 7, 4, 2], 2, 0) # 0: prices only fall
Give the machine one more piece of state: how many sales have been made. One sample test has 20,000 days and k = 20,000, which a table of n × k entries cannot finish in time, so think about when k stops mattering.
Python · Starter code · trades.py
def max_profit_k(prices, k, fee):
"""Best profit with at most k sales, a fee per sale, one crate held at a time and no rest days."""
# Replace this line with your code.
return 0 The sample tests · test_trades.py
import random
from trades import max_profit_k
PRICES = [5, 3, 6, 8, 2, 7, 4, 9]
def every_plan(prices, k, fee):
"""The answer the tests trust for a few days: every plan of waits, buys and sells, with at most k sales."""
def day(d, holding, left):
if d == len(prices):
return float("-inf") if holding else 0
best = day(d + 1, holding, left)
if holding:
best = max(best, prices[d] - fee + day(d + 1, False, left))
elif left > 0:
best = max(best, -prices[d] + day(d + 1, True, left - 1))
return best
return day(0, False, k)
def synthetic(n, seed):
"""Made-up prices from a small linear congruential generator."""
prices, x = [], seed
for _ in range(n):
x = (x * 1_103_515_245 + 12_345) % 2_147_483_648
prices.append(1 + x % 10_000)
return prices
def test_the_examples():
"""the examples of the prompt, and more sales allowed on the same prices"""
assert max_profit_k(PRICES, 0, 0) == 0
assert max_profit_k(PRICES, 1, 0) == 7
assert max_profit_k(PRICES, 2, 1) == 10
assert max_profit_k([9, 7, 4, 2], 2, 0) == 0
assert [max_profit_k(PRICES, k, 0) for k in range(5)] == [0, 7, 12, 15, 15]
assert [max_profit_k(PRICES, k, 1) for k in range(5)] == [0, 6, 10, 12, 12]
def test_one_day_and_a_large_fee():
"""one day allows no sale; a fee larger than every rise makes trading lose"""
assert max_profit_k([5], 3, 0) == 0
assert max_profit_k(PRICES, 3, 7) == 0
def test_small_cases_against_every_plan():
"""200 random cases of up to 10 days, against every plan"""
rng = random.Random(53)
for _ in range(200):
prices = [rng.randint(1, 15) for _ in range(rng.randint(1, 10))]
k, fee = rng.randint(0, 5), rng.randint(0, 3)
assert max_profit_k(prices, k, fee) == every_plan(prices, k, fee), f"max_profit_k({prices}, {k}, {fee})"
def test_two_thousand_days_with_ten_sales():
"""2,000 made-up prices, at most 10 sales and a fee of 30"""
assert max_profit_k(synthetic(2_000, 7), 10, 30) == 99_159
def test_when_k_stops_mattering():
"""20,000 days with k = 20,000: a table of n × k entries would need 4 × 10^8 steps"""
prices = synthetic(20_000, 99)
assert max_profit_k(prices, 20_000, 40) == 32_691_476
assert max_profit_k(prices, 3, 40) == 29_876 JavaScript · Starter code · trades.mjs
/** Best profit with at most k sales, a fee per sale, one crate held at a time and no rest days. */
export function maxProfitK(prices, k, fee) {
// Replace this line with your code.
return 0;
} The sample tests · trades.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { maxProfitK } from './trades.mjs';
const PRICES = [5, 3, 6, 8, 2, 7, 4, 9];
/** The answer the tests trust for a few days: every plan of waits, buys and sells, with at most k sales. */
function everyPlan(prices, k, fee) {
function day(d, holding, left) {
if (d === prices.length) return holding ? -Infinity : 0;
let best = day(d + 1, holding, left);
if (holding) best = Math.max(best, prices[d] - fee + day(d + 1, false, left));
else if (left > 0) best = Math.max(best, -prices[d] + day(d + 1, true, left - 1));
return best;
}
return day(0, false, k);
}
/** Made-up prices from a small linear congruential generator (BigInt: the product passes 2 ** 53). */
function synthetic(n, seed) {
const prices = [];
let x = BigInt(seed);
for (let i = 0; i < n; i++) {
x = (x * 1_103_515_245n + 12_345n) % 2_147_483_648n;
prices.push(1 + Number(x % 10_000n));
}
return prices;
}
test('the examples of the prompt, and more sales allowed on the same prices', () => {
assert.equal(maxProfitK(PRICES, 0, 0), 0);
assert.equal(maxProfitK(PRICES, 1, 0), 7);
assert.equal(maxProfitK(PRICES, 2, 1), 10);
assert.equal(maxProfitK([9, 7, 4, 2], 2, 0), 0);
assert.deepEqual([0, 1, 2, 3, 4].map((k) => maxProfitK(PRICES, k, 0)), [0, 7, 12, 15, 15]);
assert.deepEqual([0, 1, 2, 3, 4].map((k) => maxProfitK(PRICES, k, 1)), [0, 6, 10, 12, 12]);
});
test('one day allows no sale; a fee larger than every rise makes trading lose', () => {
assert.equal(maxProfitK([5], 3, 0), 0);
assert.equal(maxProfitK(PRICES, 3, 7), 0);
});
test('150 random cases of up to 9 days, against every plan', () => {
let s = 53;
const next = (n) => {
s = (s * 48_271) % 2_147_483_647;
return s % n;
};
for (let t = 0; t < 150; t++) {
const prices = Array.from({ length: 1 + next(9) }, () => 1 + next(15));
const k = next(6);
const fee = next(4);
assert.equal(maxProfitK(prices, k, fee), everyPlan(prices, k, fee), `maxProfitK([${prices}], ${k}, ${fee})`);
}
});
test('2,000 made-up prices, at most 10 sales and a fee of 30', () => {
assert.equal(maxProfitK(synthetic(2_000, 7), 10, 30), 99_159);
});
test('20,000 days with k = 20,000: a table of n × k entries would need 4 × 10^8 steps', () => {
const prices = synthetic(20_000, 99);
assert.equal(maxProfitK(prices, 20_000, 40), 32_691_476);
assert.equal(maxProfitK(prices, 3, 40), 29_876);
}); A hint
Keep two values for every count t from 1 to k: the best profit while holding the crate of trade t, and the best profit after finishing t trades. Buying trade t starts from "t − 1 trades finished". And over n days, how many trades can a plan make at most?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 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
4 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.
The machine proved, in five languages, and tested Pro: opens with a Pro, Premium or Ultimate pass
Three lines of updates are easy to get almost right. Here they are proved correct, run in five languages on 100,000 days and checked against the search on 2,000 random cases.
In this part:
- The invariant, and why yesterday’s values matter
- The code in five languages
- Checked against every plan on 2,000 random cases
- Rebuilding the trades behind the profit
4 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).
At most k sales: the table, the n / 2 rule and a faster search, measured Pro: opens with a Pro, Premium or Ultimate pass
A cap on the number of sales turns three numbers into a short table. This part builds it, proves when the cap stops mattering, measures it on 100,000 days, and replaces it with a search that needs no table at all.
In this part:
- Counting sales in the state
- Why k at or above n / 2 means no limit
- Measured: 100 sales over 100,000 days
- A penalty per sale instead of a k-sized table
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).
Five bugs reproduced, four variants and a practice set Pro: opens with a Pro, Premium or Ultimate pass
Most wrong answers come from five small slips in the updates. Each one runs below against the search until it fails, and four variants show how the machine changes when the rules do.
In this part:
- Five plausible bugs, reproduced on the smallest inputs that break them
- Four variants: what changes in the machine
- A practice set with sample answers
2 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).
Interview questions
-
Fresher to mid · Warm-up
Why must every update of a buy-and-sell state machine read only the previous day's values?
Because each arrow of the machine is one action on one day, and the rules allow one action a day. The new numbers for day d must each describe a plan that ended day d − 1 in some state and then made exactly one move. If the update for rest read today's new value of sold, a crate sold today would count as rested today, and the next purchase could come a day early, which breaks the rest rule. If hold read today's new rest, a sale and a purchase could fall on the same day. So compute all the new values from the old ones first, and only then replace the old ones.
-
Fresher to mid
The trading game changes: a trader may hold up to two crates at once, may buy or sell one crate a day, pays a fee on every sale and has no rest days. Design the states and the transitions, show that one action a day is enforced, and give the cost for n days.
-
Mid to senior
After each sale the trader must now rest for c days, with c up to 10⁴ and n up to 10⁵ days. How does the three-state machine change, and what does it cost in time and memory? What changes if the rest after a sale depends on that sale's price?
-
Senior to staff
At most k sales over n = 10⁵ days, with k = 2 × 10⁴ and a fee per sale. The n × k table needs 2 × 10⁹ steps. What do you do instead, why is it correct, and how fast is it?
Key takeaways
- A state-machine DP keeps one best value per state per step, computed from the previous step through the allowed moves.
- For buying and selling with a rest day, the states are hold, sold and rest; the answer is the better of sold and rest on the last day.
- Draw the diagram first: every arrow is one term of an update, and a missing arrow is a rule.
- The search over plans grows by about 1.75 times per day; the machine is O(n) time and O(1) memory.
References
- NIST Dictionary of Algorithms and Data Structures: finite state machine (National Institute of Standards and Technology (NIST))
- NIST Dictionary of Algorithms and Data Structures: dynamic programming (National Institute of Standards and Technology (NIST))
- Generalized Lagrange multiplier method for solving problems of optimum allocation of resources (H. Everett, Operations Research 11(3), 1963) (INFORMS)
- Assignment statements: the right-hand side is evaluated first (The Python Language Reference) (Python Software Foundation)
- collections.deque (The Python Standard Library) (Python Software Foundation)
- ECMAScript 2026 Language Specification: destructuring assignment (Ecma International)
- ECMAScript 2026 Language Specification: BigInt objects (Ecma International)
- The Java Language Specification, Java SE 25: 4.2.1 Integral types and values (Oracle)
- The Go Programming Language Specification: Assignment statements (The Go Authors)
- UndefinedBehaviorSanitizer (Clang documentation) (The LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress