AlgoMaster Logo

State Machine DP

Low Priority9 min readUpdated May 30, 2026
Listen to this chapter
Unlock Audio

State Machine DP applies when a problem can be modeled as a sequence of states with defined transitions. Instead of thinking in terms of indices alone, you track the current state and how it evolves based on each decision.

This pattern shows up whenever a problem restricts how you can move between different phases or statuses. The stock trading problems on LeetCode are the canonical example, but the idea extends to other problem families as well.

What Is State Machine DP?

In regular DP problems like climbing stairs or coin change, you track a single thing: the position you are at or the amount remaining. The state is a single number.

State machine DP adds another dimension. At each step, you are not only at position i, you are also in a particular mode or status. The same position can have different optimal values depending on the mode.

For example, in a stock trading problem, being on day 5 while holding a stock is a different state from being on day 5 with no stock. These are two separate states, even though the day is the same.

The core components of state machine DP are:

  1. States: The distinct modes your system can be in (holding stock, resting, in cooldown)
  2. Transitions: The rules for moving between states (buy, sell, wait)
  3. DP arrays: One array per state, tracking the optimal value at each step
  4. Base cases: The initial values for each state at the start

State machine DP differs from regular DP in this way: regular DP has one recurrence relation, while state machine DP has one recurrence relation per state, and they reference each other.

How to Identify State Machine DP Problems

Not every DP problem needs a state machine. The signals that point to this pattern:

  • Multiple modes or phases: The problem has distinct statuses like "holding", "not holding", "in cooldown"
  • Transition constraints: You cannot do certain things back-to-back (cooldown periods, transaction limits)
  • Costs that depend on current mode: Buying has a cost, selling has a profit, and which one you can do depends on your current state
  • Sequential decisions with rules: At each step, your available actions depend on what you did before
Problem TypeStatesKey Constraint
Stock with CooldownHeld, Sold, RestMust rest one day after selling
Stock with FeeHeld, Not HeldFee charged on each transaction
Stock with K TransactionsHeld(k), Not Held(k)Limited number of buy-sell pairs
Ninja Training / Paint HouseActivity 1, Activity 2, ...Cannot repeat same activity consecutively

The common thread is that the problem maps to "I am in state X, and from here I can go to state Y or Z." If the problem distinguishes between modes (holding vs not holding, etc.), state machine DP is the pattern.

The Core Idea

The recipe for state machine DP:

  1. Draw the state machine. Identify all possible states and label each transition with its cost or reward.
  2. Define one DP value per state. Usually the maximum profit (or minimum cost) achievable up to step i while in that state.
  3. Write the transitions as recurrence relations. Each arrow in the diagram becomes a term in the recurrence.
  4. Set base cases and iterate.

Apply this to the stock problem family. The simplest version, Buy and Sell Stock with unlimited transactions, has two states (held and notHeld) with buy and sell arrows between them. The recurrences are immediate:

  • held[i] = max(held[i-1], notHeld[i-1] - prices[i]) (either keep holding, or buy today)
  • notHeld[i] = max(notHeld[i-1], held[i-1] + prices[i]) (either keep resting, or sell today)

The variants in the table above all reuse this template; the diagram changes, the recipe does not.

Example Walkthrough: Best Time to Buy and Sell Stock with Cooldown (LeetCode 309)

Problem Statement

You are given an array prices where prices[i] is the price of a stock on day i. You can complete as many transactions as you like (buy one, sell one, buy one, sell one, ...) with the following constraint: after you sell, you must wait one day before buying again (cooldown period). You cannot hold multiple shares at once. Find the maximum profit.

Setting Up The States

Without the cooldown, you could buy and sell greedily. The mandatory rest day after selling means you sometimes want to delay selling or skip a profitable opportunity to set up a better one.

This maps to three states:

  1. Held: You own a share of stock. You either bought it today or kept holding from before.
  2. Sold: You sold your stock today. Tomorrow you must cool down.
  3. Rest: You do not own stock and you are not in cooldown. You are free to buy or continue resting.

Reading directly from the diagram, the recurrence relations are:

  • held[i] = max(held[i-1], rest[i-1] - prices[i]) (keep holding, or buy from rest state)
  • sold[i] = held[i-1] + prices[i] (can only sell if we were holding)
  • rest[i] = max(rest[i-1], sold[i-1]) (keep resting, or transition from sold via cooldown)

Step-by-Step Trace

Trace prices = [1, 2, 3, 0, 2].

Base cases (day 0, price = 1):

  • held[0] = -prices[0] = -1 (bought on day 0)
  • sold[0] = -infinity (cannot sell without first holding)
  • rest[0] = 0 (no position, no cost)

Applying the recurrences day by day:

DayPriceheld[i] = max(held[i-1], rest[i-1] - price)sold[i] = held[i-1] + pricerest[i] = max(rest[i-1], sold[i-1])
01-1-inf0
12max(-1, 0 - 2) = -1-1 + 2 = 1max(0, -inf) = 0
23max(-1, 0 - 3) = -1-1 + 3 = 2max(0, 1) = 1
30max(-1, 1 - 0) = 1-1 + 0 = -1max(1, 2) = 2
42max(1, 2 - 2) = 11 + 2 = 3max(2, -1) = 2

The answer is max(sold[4], rest[4]) = max(3, 2) = 3.

The optimal strategy that achieves profit 3: buy on day 0 at price 1, sell on day 1 at price 2 (profit 1), cooldown on day 2, buy on day 3 at price 0, sell on day 4 at price 2 (profit 2). Total = 3.

The greedier-looking plan "buy day 0, sell day 2 for profit 2, then buy day 3 and sell day 4" fails because selling on day 2 forces cooldown on day 3, so day 3 cannot be a buy day. The state machine handles this without extra bookkeeping: held[3] = 1 is reached via rest[2] - prices[3] = 1 - 0, where rest[2] = 1 came from sold[1] = 1 flowing through the cooldown, not from a sell on day 2.

Implementation

Complexity Analysis

  • Time: O(n), where n is the number of days. We iterate through the prices array once.
  • Space: O(1). We only store three variables regardless of input size. With the full DP array approach the space would be O(n), but since each day depends on the previous day, we reduce to constant space.

A Third Example: Best Time to Buy and Sell Stock IV (LC 188)

This is the most general stock state machine and the standard case for adding a transaction-count dimension. You are given an array prices and an integer k. Complete at most k buy-sell transactions, holding at most one share at a time, and return the maximum profit.

Why A Third Dimension

The earlier variants used two or three states per day (held vs notHeld, or held / sold / rest). When the number of allowed transactions is bounded, day-plus-holding is no longer enough state: two histories that arrive at the same day in the same holding state might have used different numbers of transactions, and only one of them might still have transactions left.

The state grows to three dimensions:

  • dp[i][j][0] = max profit on day i, having used j transactions, not holding a stock.
  • dp[i][j][1] = max profit on day i, having used j transactions, holding a stock.

A transaction is counted at the moment of buying. Selling completes a transaction that was already counted; it does not consume a new one.

Recurrence

Not holding, day i, j transactions used:

dp[i][j][0] = max( dp[i-1][j][0], // rest, do nothing

dp[i-1][j][1] + prices[i] ) // sell what we held

Holding, day i, j transactions used:

dp[i][j][1] = max( dp[i-1][j][1], // keep holding

dp[i-1][j-1][0] - prices[i] ) // buy, consuming one transaction

The j - 1 on the buy transition is the bookkeeping that makes this work: holding a stock today with j transactions used means yesterday we had j - 1 transactions used and were not holding, then we bought.

Base Cases

  • dp[0][j][0] = 0 for all j (started flat, no profit, no shares).
  • dp[0][j][1] = -prices[0] for j >= 1 (bought on day 0 using one transaction).
  • dp[0][0][1] = -infinity (cannot hold without buying, and j = 0 forbids buying).

Answer

max over j in [0, k] of dp[n-1][j][0]. The final state must not be holding; any leftover holding cannot be cashed out.

Special Case: k Is Large

LC 188 allows k up to 10^9. When k >= n / 2, the transaction budget is effectively unlimited: there are at most n / 2 non-overlapping buy-sell pairs in n days, so allocating more does not help. Fall back to the unlimited-transactions formula: sum of all positive consecutive price deltas. Without this early exit, allocating a (n+1) x (k+1) x 2 table runs out of memory.

Implementation

Sanity Check

For prices = [3, 2, 6, 5, 0, 3], k = 2, the optimal plan is buy at 2, sell at 6 (profit 4), then buy at 0, sell at 3 (profit 3). Total 7. Tracing the DP arrays day by day reproduces sell[2] = 7 on the final day.

Complexity Analysis

  • Time: O(n * k) after the special-case bailout. Each day updates each of the k transaction slots once.
  • Space: O(k). Two arrays of size k + 1.

Without the k >= n / 2 bailout, the worst-case complexity would be O(n * k) with k = 10^9, which is infeasible.

How The Variants Compare

The four buy/sell variants in this chapter share a single template and differ only in which transition is allowed:

VariantStatesExtra ConstraintTransition Difference
Stock I (LC 121, one transaction)held, notHeldAt most 1 buyBuy transition uses 0 as the base (initial cash), not prior notHeld
Stock II (unlimited transactions)held, notHeldNoneBuy: notHeld[i-1] - price. Sell: held[i-1] + price
Stock with Cooldownheld, sold, restForced rest day after sellheld[i] can only re-buy from rest[i-1], not sold[i-1]
Stock with Transaction Feeheld, notHeldFee paid per sellSell transition: held[i-1] + price - fee
Stock IV (at most k transactions)held, notHeld, transactions usedBounded transactionsBuy transition links held[j] to notHeld[j-1], advancing the counter

The general recipe is: draw the state diagram, label each transition with what it adds to the profit (or subtracts), and apply the bookkeeping for any constraints that require additional state dimensions.

Quiz

State Machine DP Quiz

10 quizzes