Builds on Linear DP, which you haven’t finished yet.
State Machine DP (Buy/Sell Stock)
You get an integer array prices: on day i, one share of a stock costs prices[i]. On each day you may buy a share, sell the share you own, or both, but you can never own more than one share at a time. Selling and then buying again on the same day is allowed, and there is no limit on the number of trades.
Return the largest total profit you can make. If no trade helps, the answer is 0.
1 and sell on day 2 at 5 for +4, then buy on day 3 at 3 and sell on day 4 at 6 for +3. Total 7.State Machine DP
states (hold, sold, rest) ➔ day-by-day transitions ➔ best final state| 1 | def max_profit(prices: list[int]) -> int: |
| 2 | """Best Time to Buy and Sell Stock II: 2-state machine, unlimited transactions. |
| 3 | States: hold (own one share), cash (own no share) |
| 4 | Time: O(N) | Space: O(1) |
| 5 | """ |
| 6 | # hold, cash: best profit so far while owning one share (hold) or owning none (cash) |
| 7 | hold = -prices[0] |
| 8 | cash = 0 |
| 9 | |
| 10 | for i in range(1, len(prices)): |
| 11 | p = prices[i] |
| 12 | # 1. Transition Both States From Yesterday: |
| 13 | # hold keeps the share or buys today (cash - p); cash stays out or sells today (hold + p). |
| 14 | # Both right-hand sides read yesterday's hold and cash, before either is overwritten. |
| 15 | hold, cash = max(hold, cash - p), max(cash, hold + p) |
| 16 | |
| 17 | # Ending while still holding a share is never better than ending with it sold. |
| 18 | return cash |
hold is the best profit so far while owning one share and cash the best profit so far while owning none.At the end of each day you are in exactly one of two states, owning a share (hold) or not (cash), and each state's best profit follows only from yesterday's two values: hold = max(hold, cash - p) keeps the share or buys today, and cash = max(cash, hold + p) stays out or sells today. Every legal run of buys and sells is a path through these states and each line keeps only the best way to arrive, so the last cash is the best profit of all; a cooldown, a fee or a trade limit just adds a state or a term, as long as every update reads yesterday's values. The trap is the start: hold = -prices[0], not 0, because hold = 0 means owning a share you never paid for, and selling it would add its full price to cash as profit.
See it run
State Machine DP on prices [7, 1, 5, 3, 6, 4], step by step
hold -7cash 0
Each column is a day. The hold row and the cash row show the best value at the end of that day.
pthe price of the current dayreadyesterday's hold and cash, read firstwritethe day whose hold and cash are written
hold = -prices[0] = -7, because buying on day 0 costs 7. cash = 0, because nothing has been sold.
Gotcha 1 of 4
- The trap
Gotcha 1: Starting with
hold = 0instead ofhold = -prices[0]Mistakehold = 0Fixhold = -prices[0]What happens:
holdis the best profit while owning one share.hold = 0means owning a share that cost nothing. Selling it adds its full price tocashas profit. Buying on day 0 costsprices[0], sohold = -prices[0].Example: prices = [5, 3]Mistake: returns 3Fix: returns 0
- Each step is only take or skip, with no mode that carries over to the next step, as in House Robber (LC 198): the best answers for the last two indices already say everything. See House Robber
Deep Dive
Why two numbers are enough
At the end of a day you either own one share or you own none. There is no third case. So two numbers describe every possible history: hold and cash.
hold is the best profit so far that ends the day owning one share. cash is the best profit so far that ends the day owning none. Profit already counts the money spent on buying, so hold can be negative.
The way the shares were bought and sold in the past does not change what you may do tomorrow. Only the mode matters: you can sell if you own a share, and you can buy if you do not. This is why the past can be reduced to one best value per mode.
Why each update line is correct
hold today comes from two sources. Either you already owned the share yesterday, which gives hold, or you bought it today from a no-share day, which gives cash - p. The better of the two is max(hold, cash - p).
cash today also comes from two sources. Either you owned nothing yesterday, which gives cash, or you sold today, which gives hold + p. The better of the two is max(cash, hold + p).
Every legal sequence of buys and sells is a path through these two modes. Each line keeps only the best way to arrive in its mode, and a better arrival can never lead to a worse finish. So the last cash is the best profit over all legal sequences.
Example: for prices = [7, 1, 5, 3, 6, 4], the pairs (hold, cash) after days 1 to 5 are (-1, 0), (-1, 4), (1, 4), (1, 7) and (3, 7). The answer is 7, which is buy at 1 and sell at 5, then buy at 3 and sell at 6.
Why the tuple assignment matters
The line hold, cash = max(hold, cash - p), max(cash, hold + p) reads the old hold and the old cash on the right side before it writes either one. Both new values come from yesterday.
For unlimited transactions, writing the two lines one after the other gives the same answer. A buy and a sale on the same day earns 0, so reading today's hold does no harm. That is a property of this one problem and not a rule.
In Best Time to Buy and Sell Stock with Cooldown (LC 309), a buy must start from the cash of two days back. A value that was already overwritten gives a wrong result. So read all old values first, and write them together.
The cost: modes times days
The card's complexity line is O(N) time and O(1) space. There are 2 modes and N days. Each day does a fixed amount of work for each mode, so the time is 2 * N steps.
Only yesterday's values are read. So two variables replace a table of N rows, and the space is constant.
More modes cost more per day, not more days. A cooldown adds one mode. A limit of k transactions adds k pairs of modes, so Best Time to Buy and Sell Stock IV (LC 188) takes O(N * k) time. For a fee, as in Best Time to Buy and Sell Stock with Transaction Fee (LC 714), the number of modes stays 2 and only one term changes.
60-Second Pitch
"At the end of each day I am either holding one share or holding none. So I keep two numbers: hold and cash. Each one is the best profit so far that ends the day in that mode. Tomorrow's hold is the better of keeping the share and buying from cash. Tomorrow's cash is the better of staying out and selling from hold. Both read yesterday's values, so I assign them together. The start is hold = -prices[0], because buying on day 0 costs money. I return cash at the end. That is O(N) time and O(1) space. If the problem adds a cooldown or a limit of k trades, I add one mode or k pairs of modes, and the same idea still works."
Common Questions
- Why do you return
cashand not the larger ofholdandcash? At the end, a share that is still owned has no value for profit. Selling it on the last day gives
cash >= hold. Somax(hold, cash)always equalscash, andreturn cashis enough.- How do you add a cooldown of one day?
A buy must start from the
cashof two days back. Keep that value in a variable such ascash_before. Each day, compute the newholdfromcash_before - p, then movecashintocash_before. For[1, 2, 3, 0, 2]the answer is 3.- How do you handle at most k transactions?
Add the number of completed buys to the state. Keep
buy[j]andsell[j]forjfrom 1 tok. Usebuy[j] = max(buy[j], sell[j - 1] - p)andsell[j] = max(sell[j], buy[j] + p). The time is O(N * k) and the space isO(k).- Is a greedy solution also correct for Best Time to Buy and Sell Stock II (LC 122)?
Yes. For unlimited trades without a fee or a cooldown, adding every positive daily rise gives the same answer. The state machine is the general method. It still works when a fee, a cooldown or a limit makes the greedy rule wrong.
- What is the time and space complexity, and why?
O(N)time andO(1)space. There are 2 modes and N days, and each day does constant work for each mode. Only yesterday's two values are read, so no table is stored.
SWE Reasoning
Spot it
- At each step you are in one mode out of a few: owning or not owning, or one of a small set of colors.
- The rules for the next step depend only on the current mode, for example you can sell only when you own a share.
- The problem adds a rule that looks at the previous step: a cooldown day, a fee on each trade, a color that may not repeat, or a limit on the number of trades.
- A brute force would try buy, sell or wait on every day. That is 3 choices per day and 3^N paths, while the number of different modes is tiny.
- You want one best total, such as maximum profit or minimum cost, and not a list of the choices.
The invariant, step by step
- Before the loop,
hold = -prices[0]andcash = 0. This is the state after day 0. Buying costsprices[0], and doing nothing earns 0. - A new price
parrives. The loop readsholdandcashas they were at the end of yesterday. - It computes the new
holdas the better of keeping the share (hold) and buying today (cash - p). - It computes the new
cashas the better of staying out (cash) and selling today (hold + p). - Both new values are assigned together. Now
holdandcashdescribe the end of today, and nothing from yesterday is still needed. - After the last day,
cashis the best profit with no share owned. Selling the share on the last day is never worse than keeping it, soreturn cashis the answer.
Traps
- Wrong start.
hold = 0means owning a share that cost nothing. A later sale adds its full price as profit. Forprices = [5, 3], this returns 3 instead of 0. - Reading an overwritten value. In Best Time to Buy and Sell Stock with Cooldown (LC 309), reading today's
cashinstead ofcash_beforeallows a buy right after a sale. For[1, 2, 3, 0, 2]the result is 4 instead of 3. - Fee on both sides. One transaction is a buy plus a sale, and the fee is paid once. In Best Time to Buy and Sell Stock with Transaction Fee (LC 714), charging it twice gives 4 instead of 8 for
[1, 3, 2, 8, 4, 9]withfee = 2. - Too few modes. With a limit on transactions,
holdandcashdo not count how many trades were used. For[3, 3, 5, 0, 0, 3, 1, 4]and at most 2 trades, two modes return 8 instead of 6. Best Time to Buy and Sell Stock III (LC 123) needs the count in the state. - Returning
hold.holdis the best profit while still owning a share, so it is never better thancashat the end. Returncash.
Practice
Builds on Linear DP, which you haven’t finished yet.
State Machine DP (Buy/Sell Stock)
You get an integer array prices: on day i, one share of a stock costs prices[i]. On each day you may buy a share, sell the share you own, or both, but you can never own more than one share at a time. Selling and then buying again on the same day is allowed, and there is no limit on the number of trades.
Return the largest total profit you can make. If no trade helps, the answer is 0.
1 and sell on day 2 at 5 for +4, then buy on day 3 at 3 and sell on day 4 at 6 for +3. Total 7.State Machine DP
states (hold, sold, rest) ➔ day-by-day transitions ➔ best final state| 1 | def max_profit(prices: list[int]) -> int: |
| 2 | """Best Time to Buy and Sell Stock II: 2-state machine, unlimited transactions. |
| 3 | States: hold (own one share), cash (own no share) |
| 4 | Time: O(N) | Space: O(1) |
| 5 | """ |
| 6 | # hold, cash: best profit so far while owning one share (hold) or owning none (cash) |
| 7 | hold = -prices[0] |
| 8 | cash = 0 |
| 9 | |
| 10 | for i in range(1, len(prices)): |
| 11 | p = prices[i] |
| 12 | # 1. Transition Both States From Yesterday: |
| 13 | # hold keeps the share or buys today (cash - p); cash stays out or sells today (hold + p). |
| 14 | # Both right-hand sides read yesterday's hold and cash, before either is overwritten. |
| 15 | hold, cash = max(hold, cash - p), max(cash, hold + p) |
| 16 | |
| 17 | # Ending while still holding a share is never better than ending with it sold. |
| 18 | return cash |
hold is the best profit so far while owning one share and cash the best profit so far while owning none.At the end of each day you are in exactly one of two states, owning a share (hold) or not (cash), and each state's best profit follows only from yesterday's two values: hold = max(hold, cash - p) keeps the share or buys today, and cash = max(cash, hold + p) stays out or sells today. Every legal run of buys and sells is a path through these states and each line keeps only the best way to arrive, so the last cash is the best profit of all; a cooldown, a fee or a trade limit just adds a state or a term, as long as every update reads yesterday's values. The trap is the start: hold = -prices[0], not 0, because hold = 0 means owning a share you never paid for, and selling it would add its full price to cash as profit.
See it run
State Machine DP on prices [7, 1, 5, 3, 6, 4], step by step
hold -7cash 0
Each column is a day. The hold row and the cash row show the best value at the end of that day.
pthe price of the current dayreadyesterday's hold and cash, read firstwritethe day whose hold and cash are written
hold = -prices[0] = -7, because buying on day 0 costs 7. cash = 0, because nothing has been sold.
Gotcha 1 of 4
- The trap
Gotcha 1: Starting with
hold = 0instead ofhold = -prices[0]Mistakehold = 0Fixhold = -prices[0]What happens:
holdis the best profit while owning one share.hold = 0means owning a share that cost nothing. Selling it adds its full price tocashas profit. Buying on day 0 costsprices[0], sohold = -prices[0].Example: prices = [5, 3]Mistake: returns 3Fix: returns 0
- Each step is only take or skip, with no mode that carries over to the next step, as in House Robber (LC 198): the best answers for the last two indices already say everything. See House Robber
Deep Dive
Why two numbers are enough
At the end of a day you either own one share or you own none. There is no third case. So two numbers describe every possible history: hold and cash.
hold is the best profit so far that ends the day owning one share. cash is the best profit so far that ends the day owning none. Profit already counts the money spent on buying, so hold can be negative.
The way the shares were bought and sold in the past does not change what you may do tomorrow. Only the mode matters: you can sell if you own a share, and you can buy if you do not. This is why the past can be reduced to one best value per mode.
Why each update line is correct
hold today comes from two sources. Either you already owned the share yesterday, which gives hold, or you bought it today from a no-share day, which gives cash - p. The better of the two is max(hold, cash - p).
cash today also comes from two sources. Either you owned nothing yesterday, which gives cash, or you sold today, which gives hold + p. The better of the two is max(cash, hold + p).
Every legal sequence of buys and sells is a path through these two modes. Each line keeps only the best way to arrive in its mode, and a better arrival can never lead to a worse finish. So the last cash is the best profit over all legal sequences.
Example: for prices = [7, 1, 5, 3, 6, 4], the pairs (hold, cash) after days 1 to 5 are (-1, 0), (-1, 4), (1, 4), (1, 7) and (3, 7). The answer is 7, which is buy at 1 and sell at 5, then buy at 3 and sell at 6.
Why the tuple assignment matters
The line hold, cash = max(hold, cash - p), max(cash, hold + p) reads the old hold and the old cash on the right side before it writes either one. Both new values come from yesterday.
For unlimited transactions, writing the two lines one after the other gives the same answer. A buy and a sale on the same day earns 0, so reading today's hold does no harm. That is a property of this one problem and not a rule.
In Best Time to Buy and Sell Stock with Cooldown (LC 309), a buy must start from the cash of two days back. A value that was already overwritten gives a wrong result. So read all old values first, and write them together.
The cost: modes times days
The card's complexity line is O(N) time and O(1) space. There are 2 modes and N days. Each day does a fixed amount of work for each mode, so the time is 2 * N steps.
Only yesterday's values are read. So two variables replace a table of N rows, and the space is constant.
More modes cost more per day, not more days. A cooldown adds one mode. A limit of k transactions adds k pairs of modes, so Best Time to Buy and Sell Stock IV (LC 188) takes O(N * k) time. For a fee, as in Best Time to Buy and Sell Stock with Transaction Fee (LC 714), the number of modes stays 2 and only one term changes.
60-Second Pitch
"At the end of each day I am either holding one share or holding none. So I keep two numbers: hold and cash. Each one is the best profit so far that ends the day in that mode. Tomorrow's hold is the better of keeping the share and buying from cash. Tomorrow's cash is the better of staying out and selling from hold. Both read yesterday's values, so I assign them together. The start is hold = -prices[0], because buying on day 0 costs money. I return cash at the end. That is O(N) time and O(1) space. If the problem adds a cooldown or a limit of k trades, I add one mode or k pairs of modes, and the same idea still works."
Common Questions
- Why do you return
cashand not the larger ofholdandcash? At the end, a share that is still owned has no value for profit. Selling it on the last day gives
cash >= hold. Somax(hold, cash)always equalscash, andreturn cashis enough.- How do you add a cooldown of one day?
A buy must start from the
cashof two days back. Keep that value in a variable such ascash_before. Each day, compute the newholdfromcash_before - p, then movecashintocash_before. For[1, 2, 3, 0, 2]the answer is 3.- How do you handle at most k transactions?
Add the number of completed buys to the state. Keep
buy[j]andsell[j]forjfrom 1 tok. Usebuy[j] = max(buy[j], sell[j - 1] - p)andsell[j] = max(sell[j], buy[j] + p). The time is O(N * k) and the space isO(k).- Is a greedy solution also correct for Best Time to Buy and Sell Stock II (LC 122)?
Yes. For unlimited trades without a fee or a cooldown, adding every positive daily rise gives the same answer. The state machine is the general method. It still works when a fee, a cooldown or a limit makes the greedy rule wrong.
- What is the time and space complexity, and why?
O(N)time andO(1)space. There are 2 modes and N days, and each day does constant work for each mode. Only yesterday's two values are read, so no table is stored.
SWE Reasoning
Spot it
- At each step you are in one mode out of a few: owning or not owning, or one of a small set of colors.
- The rules for the next step depend only on the current mode, for example you can sell only when you own a share.
- The problem adds a rule that looks at the previous step: a cooldown day, a fee on each trade, a color that may not repeat, or a limit on the number of trades.
- A brute force would try buy, sell or wait on every day. That is 3 choices per day and 3^N paths, while the number of different modes is tiny.
- You want one best total, such as maximum profit or minimum cost, and not a list of the choices.
The invariant, step by step
- Before the loop,
hold = -prices[0]andcash = 0. This is the state after day 0. Buying costsprices[0], and doing nothing earns 0. - A new price
parrives. The loop readsholdandcashas they were at the end of yesterday. - It computes the new
holdas the better of keeping the share (hold) and buying today (cash - p). - It computes the new
cashas the better of staying out (cash) and selling today (hold + p). - Both new values are assigned together. Now
holdandcashdescribe the end of today, and nothing from yesterday is still needed. - After the last day,
cashis the best profit with no share owned. Selling the share on the last day is never worse than keeping it, soreturn cashis the answer.
Traps
- Wrong start.
hold = 0means owning a share that cost nothing. A later sale adds its full price as profit. Forprices = [5, 3], this returns 3 instead of 0. - Reading an overwritten value. In Best Time to Buy and Sell Stock with Cooldown (LC 309), reading today's
cashinstead ofcash_beforeallows a buy right after a sale. For[1, 2, 3, 0, 2]the result is 4 instead of 3. - Fee on both sides. One transaction is a buy plus a sale, and the fee is paid once. In Best Time to Buy and Sell Stock with Transaction Fee (LC 714), charging it twice gives 4 instead of 8 for
[1, 3, 2, 8, 4, 9]withfee = 2. - Too few modes. With a limit on transactions,
holdandcashdo not count how many trades were used. For[3, 3, 5, 0, 0, 3, 1, 4]and at most 2 trades, two modes return 8 instead of 6. Best Time to Buy and Sell Stock III (LC 123) needs the count in the state. - Returning
hold.holdis the best profit while still owning a share, so it is never better thancashat the end. Returncash.