Best Time to Buy And Sell Stock With Cooldown
A medium problem from 2-D Dynamic Programming. Try it first, open a hint when you are stuck, and read the solution when you are done.
- Medium
- 2-D Dynamic Programming
The problem
prices lists the price of one share on each day. You may buy and sell as often as you like, but you can own at most one share at a time, and after a sale you must wait one full day before you buy again. Each trade earns its selling price minus its buying price. Return the largest total profit you can make. If no trade makes money, return 0.
What to write
function solve(prices: number[]): numberExample 1
Input prices = [1,2,3,0,2] Output 3
Buy on day 1 at 1 and sell on day 2 at 2, which earns 1. Day 3 is the waiting day. Buy on day 4 at 0 and sell on day 5 at 2, which earns 2. The total is 3. Selling on day 3 instead earns 2, but then day 4 is closed for buying.
Example 2
Input prices = [1] Output 0
There is only one day. You could buy on it, but there is no later day to sell on, so no trade can be completed and the profit is 0.
Constraints
Prices are nonnegative.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Use the constraints
You never have to trade, so the answer is at least 0. Without the waiting day you could collect every rise in price. With it, a sale today can block a cheap buy tomorrow, so the best move today depends on prices you have not looked at yet.
Hint 2: Ask a smaller question
Stand at the end of some day in 1, 2, 3, 0, 2. What do you need to know about your past to decide what you may do tomorrow? Is the full list of your trades needed, or only a few facts about where you stand right now?
Hint 3: Name the pattern
What you may do tomorrow depends only on how today ends: holding a share, sold today, or resting, which means no share and no sale today. For every day and each of these three situations, keep the most money you could have. Days in one direction and situations in the other make a small table: two-dimensional dynamic programming. Read the lesson on two-dimensional dynamic programming.
Hint 4: State the invariant
After each day keep three numbers: the most money you can have if the day ends holding a share, if it ends with a sale made that day, and if it ends resting, with no share and no sale that day. Money starts at 0; a buy subtracts the price, a sale adds it. Each number needs only yesterday's three.
Hint 5: Sketch the pseudocode
holding = sold = impossible (minus infinity), resting = 0. For each price: new sold = holding + price; new holding = larger of holding and (resting - price); new resting = larger of resting and sold. Replace all three together, so every right-hand side uses yesterday's values. Return the larger of resting and sold.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Approach
Track holding a stock, selling today, and resting. A buy can only follow a resting state, which enforces the cooldown.
The slow way
Try every schedule: on each day buy, sell or wait, wherever the rules allow, and keep the best result. It is correct, but the number of schedules grows fast. The 5 days of example 1 allow 21 schedules, and 30 days allow 26931732.
The insight
What you may do tomorrow depends only on how today ends: holding a share, having sold today, or resting, which means no share and no sale today. The trades that led there do not matter. So each of the three endings needs one number, the most money so far, and tomorrow's three follow from today's three. Carrying best answers forward like this is dynamic programming.
Step by step
- Start before day one: resting has 0, holding and sold are impossible. Each value is the most money you can have in that situation. Before any day you own nothing and have sold nothing, so only resting is possible. Minus infinity marks the other two as impossible: it loses every comparison.
- For each day's price, set aside yesterday's sold value first. The sold value is about to be overwritten with today's sale, but the resting update further down still needs yesterday's. Without the saved copy, resting would include a sale made today.
- Sold today: yesterday's holding value plus today's price. You can only sell a share you already held last night, and the sale brings in today's price. While holding is still impossible, this sum stays impossible too.
- Holding: the better of keeping the share and buying after a rest. You hold tonight if you held last night and kept the share, or if you were resting last night and buy today, which costs today's price. A buy never starts from the sold value: that is the waiting day.
- Resting: the better of resting yesterday and having sold yesterday. You end today with no share and no sale if yesterday ended the same way, or if you sold yesterday and today is your waiting day. This uses the saved copy, not the sold value written a moment ago.
- Return the larger of the resting value and the sold value. After the last day the best schedule owns no share, so it ends either resting or with a sale on that last day. A share still held was paid for and never sold, so holding is never better than resting.
The same numbered steps are the comments in each solution.
JavaScript
function solve(prices) {
// 1. Start before day one: resting has 0, holding and sold are impossible
let hold = -Infinity,
sold = -Infinity,
rest = 0;
// 2. For each day's price, set aside yesterday's sold value first
for (const price of prices) {
const oldSold = sold;
// 3. Sold today: yesterday's holding value plus today's price
sold = hold + price;
// 4. Holding: the better of keeping the share and buying after a rest
hold = Math.max(hold, rest - price);
// 5. Resting: the better of resting yesterday and having sold yesterday
rest = Math.max(rest, oldSold);
}
// 6. Return the larger of the resting value and the sold value
return Math.max(rest, sold);
}TypeScript
function solve(prices: number[]): number {
// 1. Start before day one: resting has 0, holding and sold are impossible
let hold = -Infinity,
sold = -Infinity,
rest = 0;
// 2. For each day's price, set aside yesterday's sold value first
for (const price of prices) {
const oldSold = sold;
// 3. Sold today: yesterday's holding value plus today's price
sold = hold + price;
// 4. Holding: the better of keeping the share and buying after a rest
hold = Math.max(hold, rest - price);
// 5. Resting: the better of resting yesterday and having sold yesterday
rest = Math.max(rest, oldSold);
}
// 6. Return the larger of the resting value and the sold value
return Math.max(rest, sold);
}
Python
def solve(prices):
# 1. Start before day one: resting has 0, holding and sold are impossible
hold = sold = float("-inf")
rest = 0
# 2. For each day's price, set aside yesterday's sold value first
for price in prices:
old_sold = sold
# 3. Sold today: yesterday's holding value plus today's price
sold = hold + price
# 4. Holding: the better of keeping the share and buying after a rest
hold = max(hold, rest - price)
# 5. Resting: the better of resting yesterday and having sold yesterday
rest = max(rest, old_sold)
# 6. Return the larger of the resting value and the sold value
return max(rest, sold)
Example 1, traced
Input prices = [1,2,3,0,2] Output 3
| Day and price | Sold today | Holding | Resting |
|---|---|---|---|
| Before day 1 | not possible | not possible | 0 |
| Day 1, price 1 | not possible | larger of not possible and (0 - 1): -1 | larger of 0 and not possible: 0 |
| Day 2, price 2 | -1 + 2 = 1 | larger of -1 and (0 - 2): -1 | larger of 0 and not possible: 0 |
| Day 3, price 3 | -1 + 3 = 2 | larger of -1 and (0 - 3): -1 | larger of 0 and 1: 1 |
| Day 4, price 0 | -1 + 0 = -1 | larger of -1 and (1 - 0): 1 | larger of 1 and 2: 2 |
| Day 5, price 2 | 1 + 2 = 3 | larger of 1 and (2 - 2): 1 | larger of 2 and -1: 2 |
| After day 5 | 3 | 1, not used | 2: the answer is the larger, 3 |
Why it works
Each value is the most money over all schedules that end the day in its situation. A schedule that sold today held a share yesterday. One that holds either held yesterday or rested yesterday and bought today. One that rests either rested or sold yesterday. These cases cover every allowed schedule and no forbidden one, since a buy never follows a sale directly.
Time and space
Time O(n), extra space O(1). Each price is handled once with a fixed handful of additions and comparisons, so the time is O(n) for n prices. A full table would have a row per day and a column per situation, but only the latest row, three numbers and one saved copy, is kept: O(1) space.
n and m are input sizes, h is a tree's height, V and E are a graph's vertices and edges. The bounds describe this reference solution.
Watch out
- Update resting from yesterday's sold value, not today's. Without the saved copy, a sale today counts as rest at once and you can buy the next morning: the prices 1, 2, 3, 0, 2 give 4 instead of 3.
- Start holding as impossible, not as 0. A 0 means you own a share that cost nothing, and that share gets sold on day one: the single price 1 returns 1 instead of 0.
- Return the larger of resting and sold, not resting alone. The best schedule often sells on the last day, and that sale has not reached the resting value yet: 1, 2, 3, 0, 2 would give 2.
Pattern
Two-dimensional dynamic programming. Builds on Best Time to Buy And Sell Stock and House Robber.