Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25.0.4.1, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 9 of 30 minutes free
Free: The problem: a trading game with a rest day · The brute force, and what it costs · What to notice · 3 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 21 minutes.

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 game, searched, with the plans counted Python · brute/every_plan.py
"""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

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

Three states, rest, hold and sold: buy moves rest to hold, sell moves hold to sold, and sold always moves back to rest the next day.One day: one move along an arrowrestfree to buyholdone cratesoldsold todaybuy− pricesell+ price − feenext day:rest (cool down)waitwaitEach state keeps the best profit of any plan ending in it.

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?

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.

  1. Question 1 of 4 After a day, hold = −3, sold = 4 and rest = 2. The next price is 2 and the fee is 1. What are hold, sold and rest after that day?

    Choose one answer.

    Show the answer to question 1

    Answer: hold 0, sold −2, rest 4

    hold = max(−3, 2 − 2) = 0, buying from yesterday's rest; sold = −3 + 2 − 1 = −2, selling yesterday's crate; rest = max(2, 4) = 4, the forced rest after yesterday's sale. This is day 4 of the lesson's dry run.

  2. Question 2 of 4 Which arrow is missing from the diagram on purpose, and what rule does its absence enforce?

    Choose one answer.

    Show the answer to question 2

    Answer: From sold to hold: no purchase on the day after a sale

    A plan that sold today reaches hold only through a day of rest, so the earliest purchase is two days after the sale. Hold to rest and rest to sold are missing too, but they would break the rules of trading itself, not the rest day; hold to hold, waiting with a crate, is in the diagram.

  3. Question 3 of 4 The search over plans tried 922,111 plans for 24 days. Each extra day multiplies the count by about 1.75. About how many plans would 26 days have, in millions? (Round to one decimal.)

    Type a number.

    Show the answer to question 3

    Answer: 2.8 million (anything from 2.7 to 2.9 counts)

    Two more days multiply the count by about 1.75 × 1.75 ≈ 3.06, so about 922,111 × 3.06 ≈ 2.8 million. The machine does 26 updates of three numbers instead.

  4. Question 4 of 4 Why is the answer the larger of sold and rest on the last day, and not hold?

    Choose one answer.

    Show the answer to question 4

    Answer: A crate still held at the end earns nothing back, and its price was already paid

    hold includes the price paid for the crate, and the game ends before it can be sold, so a plan ending in hold is never better than the same plan without that purchase. Both sold and rest are needed: the best plan may end with a sale on the last day, or with a day of rest.

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.

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.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.