Hard Problemsstocksoptimization

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 robber

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

Step not started

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.

Trades allowed (k)

The walkthrough uses this input. Finish it to unlock the others.

Rows ↓
Cols →
3
2
6
5
0
3
t=0
0
0
0
0
0
0
t=1
0
0
4
4
4
4
t=2
0
0
4
4
4
7

Rows ↓trades allowed

Cols →price on each day, day 0 at the left

stock-buy-and-sell-at-most-k-times.ts
  1. 1function maxProfitK(prices, k) {
  2. 2 const n = prices.length;
  3. 3 const dp = Array.from({ length: k + 1 }, () => new Array(n).fill(0));
  4. 4 for (let t = 1; t <= k; t++) {
  5. 5 for (let d = 1; d < n; d++) {
  6. 6 let best = dp[t][d - 1];
  7. 7 for (let j = 0; j < d; j++) {
  8. 8 best = Math.max(best, dp[t - 1][j] + prices[d] - prices[j]);
  9. 9 }
  10. 10 dp[t][d] = best;
  11. 11 }
  12. 12 }
  13. 13 return dp[k][n - 1];
  14. 14}
Base caseComputedBeing readAnswer

The code, the trap, the variations

stock-buy-and-sell-at-most-k-times.ts
  1. 1function maxProfitK(prices, k) {
  2. 2 const n = prices.length;
  3. 3 const dp = Array.from({ length: k + 1 }, () => new Array(n).fill(0));
  4. 4 for (let t = 1; t <= k; t++) {
  5. 5 for (let d = 1; d < n; d++) {
  6. 6 let best = dp[t][d - 1];
  7. 7 for (let j = 0; j < d; j++) {
  8. 8 best = Math.max(best, dp[t - 1][j] + prices[d] - prices[j]);
  9. 9 }
  10. 10 dp[t][d] = best;
  11. 11 }
  12. 12 }
  13. 13 return dp[k][n - 1];
  14. 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.