Skip to content
dsa450for five year olds

Dynamic Programming · 58 of 60

Maximum profit by buying and selling a share at most k times

Knowing all future prices, make the most money with at most k buy and sell trips, holding one share at a time.

Simple way
time O(2^n)
room O(n)
Clever way
time O(n * k)
room O(n * k)

The problem, simply

You get prices, where prices[d] is the price of one share on day d, and a number k. One trade means buy once, then sell once later. You may hold at most one share at a time, so you must sell before you buy again.

Give back the most money you can make with at most k trades. If prices only fall, the answer is 0: you do not trade at all.

A story to remember it

Picture it

arrayprice on day
012345326503

The price on each day. The best two trades are buy at 2, sell at 6, then buy at 0, sell at 3.

  • answer
  • just added

Examples

Two trades

In
prices = [3, 2, 6, 5, 0, 3], k = 2
Out
7

Buy at 2, sell at 6 for 4. Buy at 0, sell at 3 for 3. Total 7.

One trade only

In
prices = [1, 5, 2, 8], k = 1
Out
7

With one trade, buy at 1 and sell at 8. With k = 2 it would be (5 - 1) + (8 - 2) = 10.

Prices only fall

In
prices = [5, 4, 3, 2, 1], k = 3
Out
0

Every trade loses money, so do nothing.

Small

In
prices = [2, 4, 1], k = 2
Out
2

Buy at 2, sell at 4. There is no later rise to use the second trade on.

Way 1: the simple way

Walk day by day and try every move that is allowed today.

  1. If you are free (no card): rest, or buy today (pay the price).
  2. If you hold a card: rest, or sell today (get the price) and use up one trade.
  3. Stop when the days run out or no trades are left.
tree
restrestrestbuy 1buy 4restsell 1buy 2restrestsell 1sell 4d0 k1 freed1 k1 freed2 k1 freed3 k1 freed3 k1 holdd2 k1 holdd3 k1 holdd3 k0 freed1 k1 holdd2 k1 holdd3 k1 holdd3 k0 freed2 k0 free

Prices 2, 4, 1 and k = 1. d is the day, k is how many trades are left, and hold or free says if we have a card in hand. Gray circles are questions asked before. d2 k1 hold shows up twice: once after buying at 2, once after buying at 4.

  • seen
java
class Solution {
    public int maxProfit(int k, int[] prices) {
        return solve(prices, 0, k, false);
    }

    private int solve(int[] prices, int day, int left, boolean holding) {
        if (day == prices.length || left == 0) return 0;
        int rest = solve(prices, day + 1, left, holding);           // do nothing today
        if (holding) {
            int sell = prices[day] + solve(prices, day + 1, left - 1, false);
            return Math.max(rest, sell);
        } else {
            int buy = -prices[day] + solve(prices, day + 1, left, true);
            return Math.max(rest, buy);
        }
    }
}

How long it takes: two choices every day, O(2^n). Extra room: one waiting call per day, O(n). The same question (same day, same trades left, same hand) comes up many times, so we should write answers down.

Way 2: the clever way

Make a table dp[t][d]: the most money by day d using at most t trades.

  1. Row 0 is all 0 (no trades). Column 0 is all 0 (you cannot sell on the first day).
  2. For box dp[t][d], either rest: dp[t][d - 1], or sell today. Selling today means you bought on an earlier day m, with dp[t - 1][m] money before buying. That gives dp[t - 1][m] - prices[m] + prices[d].
  3. The part dp[t - 1][m] - prices[m] does not depend on today. Keep the best one so far in bestBuy, and update it as you move right.
  4. The answer is the bottom right box.

Each box gets two arrows: one from the box on its left (rest) and one from the best buy day in the row above (sell).

Fill the money table row by row

01 / 11

gridbest money by this day
day 0 (3)day 1 (2)day 2 (6)day 3 (5)day 4 (0)day 5 (3)0 trades1 trade2 trades00000000

Row 0 means no trades allowed, so it is all 0. Column day 0 is also 0: on the first day you can buy, but you cannot sell yet.

  • looking now
  • checking
  • done
  • answer
java
class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if (n <= 1 || k == 0) return 0;

        if (k >= n / 2) {                       // enough trades to catch every rise
            int profit = 0;
            for (int i = 1; i < n; i++) {
                profit += Math.max(0, prices[i] - prices[i - 1]);
            }
            return profit;
        }

        int[][] dp = new int[k + 1][n];         // row 0 and column 0 stay 0
        for (int t = 1; t <= k; t++) {
            int bestBuy = dp[t - 1][0] - prices[0];       // money left after the best buy so far
            for (int d = 1; d < n; d++) {
                dp[t][d] = Math.max(dp[t][d - 1], bestBuy + prices[d]);   // rest, or sell today
                bestBuy = Math.max(bestBuy, dp[t - 1][d] - prices[d]);     // or buy today for later
            }
        }
        return dp[k][n - 1];
    }
}

How long it takes: k rows times n days, constant work per box, O(n * k). Extra room: the table, O(n * k). Only the row above is used, so two rows of n are enough.

Watch out for

  • Update bestBuy after using it for today's box, or you may buy and sell on the same day. That is harmless here (it earns 0) but it confuses people reading the code.
  • Use row t - 1 for the buy money, not row t. Using row t lets one trade count twice.
  • An empty price list or k = 0 gives 0.

What interviewers ask next

  • Can you use O(k) memory? Yes. Keep buy[t] and sell[t] for each t and update them day by day: buy[t] = max(buy[t], sell[t - 1] - price), sell[t] = max(sell[t], buy[t] + price).
  • What about a fee per trade? Subtract the fee when you sell.
  • What about a one day cooldown after selling? Buy from the money of two days ago instead of yesterday.
  • Is there something faster for huge n and k? Yes: list each rising run as a valley and a peak, then merge the cheapest runs until only k are left using a heap. That is O(n log n).

Remember this

Grown-up words: the notebook is a 2D DP table, the round trips are transactions, bestBuy is the running maximum that removes the inner loop, and "hold or free" is the state in a state machine DP.