Stock Buy and Sell – At-Most k Times
Given a stock's price on each of n days and a limit of at most k buy-sell transactions, where you never hold more than one share at a time and each sell must follow its matching buy, find the maximum total profit achievable.
Do this lesson first: house robberExample input
prices = [3, 2, 6, 5, 0, 3], k = 2
Expected output
7
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Row 0 is all zeros: with no trades allowed nothing is earned. Column 0 is all zeros: one day cannot hold a buy and a later sale. Every other cell reads its left neighbour and the row above at earlier days, so the table fills one whole row at a time, t = 1 first. That is the order the animation uses, and it is why picking a bigger k just adds rows underneath without changing any row above.
Cols →
Rows ↓trades allowed
Cols →price on each day, day 0 at the left
- 1
function maxProfitK(prices, k) { - 2
const n = prices.length; - 3
const dp = Array.from({ length: k + 1 }, () => new Array(n).fill(0)); - 4
for (let t = 1; t <= k; t++) { - 5
for (let d = 1; d < n; d++) { - 6
let best = dp[t][d - 1]; - 7
for (let j = 0; j < d; j++) { - 8
best = Math.max(best, dp[t - 1][j] + prices[d] - prices[j]); - 9
} - 10
dp[t][d] = best; - 11
} - 12
} - 13
return dp[k][n - 1]; - 14
}
The code, the trap, the variations
- 1
function maxProfitK(prices, k) { - 2
const n = prices.length; - 3
const dp = Array.from({ length: k + 1 }, () => new Array(n).fill(0)); - 4
for (let t = 1; t <= k; t++) { - 5
for (let d = 1; d < n; d++) { - 6
let best = dp[t][d - 1]; - 7
for (let j = 0; j < d; j++) { - 8
best = Math.max(best, dp[t - 1][j] + prices[d] - prices[j]); - 9
} - 10
dp[t][d] = best; - 11
} - 12
} - 13
return dp[k][n - 1]; - 14
}
Where people go wrong
Adding up the k biggest day-to-day rises. It feels right, because a trade is a run of rises, but one trade can span several rises and the dips between them. At all ten days with k = 2 the biggest two rises are 4 and 3, so that method answers 7, while the table says 9: buy at 2 and sell at 6, then buy at 0 and hold through the dips to sell at 5. A long trade can be worth more than any single rise inside it, and a table that weighs every buy day, as this one does, sees that. The rises method is guaranteed right only once k is large enough not to bind.
Fix k = 2.
Nothing in this table changes; it just stops at row 2. stock-buy-and-sell-at-most-two-times answers the same question with five rows of positions (nothing yet, hold 1, sold 1, hold 2, sold 2), each cell reading only the day before, which is O(n) with no inner loop.
Charge a fee on every sale.
Each candidate sale becomes dp[t - 1][j] + prices[d] - prices[j] - fee. The rows, the bases and the order are unchanged.
Require a day off after each sale before the next buy.
Earlier trades must now finish by day j - 2, so the candidate reads dp[t - 1][j - 2] (0 when j < 2) instead of dp[t - 1][j]. Everything else stays.
Let k be unlimited.
Drop the t index: one row, dp[d] = max(dp[d - 1], max over j < d of dp[j] + prices[d] - prices[j]), reading its own earlier days instead of a row above. Bases and order are unchanged, and its value is the sum of every positive day-to-day change, the shortcut real solutions take once k reaches n / 2.