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
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.
- If you are free (no card): rest, or buy today (pay the price).
- If you hold a card: rest, or sell today (get the price) and use up one trade.
- Stop when the days run out or no trades are left.
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
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.
- Row 0 is all 0 (no trades). Column 0 is all 0 (you cannot sell on the first day).
- For box
dp[t][d], either rest:dp[t][d - 1], or sell today. Selling today means you bought on an earlier daym, withdp[t - 1][m]money before buying. That givesdp[t - 1][m] - prices[m] + prices[d]. - The part
dp[t - 1][m] - prices[m]does not depend on today. Keep the best one so far inbestBuy, and update it as you move right. - 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
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
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
bestBuyafter 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 - 1for the buy money, not rowt. Using rowtlets one trade count twice. - An empty price list or
k = 0gives 0.
What interviewers ask next
- Can you use
O(k)memory? Yes. Keepbuy[t]andsell[t]for eachtand 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
nandk? Yes: list each rising run as a valley and a peak, then merge the cheapest runs until onlykare left using a heap. That isO(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.