Problem
You are given an integer array prices
where prices[i]
is the price of a given stock on the ith
day, and an integer k
.
Find the maximum profit you can achieve. You may complete at most k
transactions.
Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
Examples
Example 1:
Input: k = 2, prices = [2,4,1]
Output:
2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.
Example 2:
Input:
k = 2, prices = [3,2,6,5,0,3]
Output:
7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4. Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
We have already seen - Best Time To Buy And Sell Stock 3 - at most twice. This is the generic version of that.
Solution
Method 1 - Recursion
Code
Java
class Solution {
public int maxProfit(int k, int[] prices) {
return dfs(k, prices.length, prices);
}
public int dfs(int k, int n, int[] prices) {
if (k == 0 || n == 0)
return 0; // if any of then becomes 0, no profit
int curr = prices[n - 1], profit = 0;
// Finding max profit for current day. As we are moving backwards, if we
// sell on day let say x, and purchase it on y (where y<x) then the next
// possible transaction would be from day y. That's why we use function
// solve() again while getting our profit. Also as we have transacted
// once, we reduce the value of k.
for (int i = n - 1; i >= 0; i--) {
profit =
Math.max(profit, (curr - prices[i]) + dfs(k - 1, i, prices));
}
// if we decide not to transact on that day, we simply reduce n, else go
// with our 'profit'.
return Math.max(dfs(k, n - 1, prices), profit);
}
}
Method 2 - Top Down DP with Memoization
Code
Java
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
int[][] dp = new int[k + 1][n + 1];
for (int[] row : dp) {
Arrays.fill(row, -1);
}
return dfs(k, n, prices, dp);
}
public int dfs(int k, int n, int[] prices, int[][] dp) {
if (k == 0 || n == 0) {
return 0; // No profit if either k or n is zero
}
if (dp[k][n] != -1) {
return dp[k][n];
}
int curr = prices[n - 1];
int maxProfit = 0;
// Calculate max profit for the current day.
// By moving backwards and selling on day p, buying on day q (where q <
// p), the next transaction would start from day q. Each time we
// transact, k is decreased by 1.
for (int i = n - 1; i >= 0; i--) {
maxProfit = Math.max(
maxProfit, (curr - prices[i]) + dfs(k - 1, i, prices, dp));
}
// If no transaction on the current day, proceed by reducing n;
// otherwise, use the computed maxProfit.
dp[k][n] = Math.max(dfs(k, n - 1, prices, dp), maxProfit);
return dp[k][n];
}
}
Method 3 - DP
Let’s create a dp
array of size [k+1][n]
. For any t
in [1, k]
and i
in [1, n)
:
Let dp[t][i]
denote the maximum profit using at most t
transactions up to day i
(inclusive). The relation can be expressed as:
dp[t][i] = max(dp[t][i-1], max(prices[i] - prices[j] + dp[t-1][j]))
for all j in the range [0, i-1]
dp[t][i-1] -> If no transaction is made on day i
dp[t-1][j] -> Best profit possible with t-1 transactions on day j
To summarize, dp[t][i]
is the maximum of:
dp[t][i-1]
, representing the scenario where no transaction is made on the ith day.- The maximum profit from selling on the ith day. To sell shares on the ith day, they must be bought on any day in the range
[0, i-1]
. If shares are bought on the jth day and sold on the ith day, the maximum profit isprices[i] - prices[j] + dp[t-1][j]
, wherej
varies from 0 to i-1. Here,dp[t-1][j]
represents the best we could have done with one less transaction by the jth day.
Code
Java
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int maxProfit = 0;
int n = prices.length;
int[][] dp = new int[k + 1][n];
for (int t = 1; t<= k; t++) {
for (int i = 1; i<n; i++) {
for (int j = 0; j<i; j++) {
dp[t][i] = Math.max(dp[t][i], prices[i] - prices[j] + dp[t - 1][j]);
}
dp[t][i] = Math.max(dp[t][i], dp[t][i - 1]);
}
}
return dp[k][n - 1];
}
Complexity
- ⏰ Time complexity:
O(k.n^2)
- 🧺 Space complexity:
O(k.n)
Method 4 - Optimized DP
The previous solution has a time complexity of O(k * n^2)
. However, this can be optimized to calculate the maximum profit from selling shares on the i
th day in constant time.
Consider the previous DP relation:
dp[t][i] = max(profit [t][i-1], max(prices[i] – prices[j] + dp[t-1][j]))
for all j in range [0, i-1]
Notice that:
max(prices[i] – prices[j] + dp[t-1][j])
for all j in range [0, i-1]
can be rewritten as:
= prices[i] + max(dp[t-1][j] – prices[j])
for all j in range [0, i-1]
= prices[i] + max(prevDiff, dp[t-1][i-1] – prices[i-1])
where prevDiff is max(dp[t-1][j] – prices[j])
for all j in range [0, i-2]
If we’ve already computed max(dp[t-1][j] - prices[j])
for all j
in the range [0, i-2]
, then we can calculate it for j = i-1
in constant time. This eliminates the need to look back through the range [0, i-1]
to find the best day to buy. Instead, we can determine it in constant time using this revised relation:
dp[t][i] = max(dp[t][i-1], prices[i] + max(prevDiff, dp[t-1][i-1] - prices[i-1]))
where prevDiff is max(dp[t-1][j] - prices[j]) for all j in the range [0, i-2]
To summarize:
maxDiff = Max(maxDiff,dp[t-1][i-1] - prices[i-1])
dp[t][i] = Max(dp[t][i-1],prices[i]+maxDiff)
dp[t][i-1]
: If no transaction is made on the ith dayprices[i] - prices[j] + dp[t-1][j]
: If a transaction is made on the ith day, computemaxDiff
for the range[0, i-1]
. This reuses the previously computedmaxDiff
, reducing the time complexity toO(n)
.
Code
Java
public int maxProfit(int k, int[] prices) {
if (prices.length == 0)
return 0;
int n = prices.length;
int dp[][] = new int[k + 1][n];
for (int t = 1; i<= k; t++) {
int maxDiff = dp[t - 1][0] - prices[0];
for (int i = 1; i<n; i++) {
maxDiff = Math.max(maxDiff, dp[t - 1][i - 1] - prices[i - 1]);
dp[t][i] = Math.max(dp[t][i - 1], prices[j] + maxDiff);
}
}
return dp[k][n - 1];
}
We can also combine [[Best Time To Buy And Sell Stock 2 - Any number of times]], as that deals with the case of k = infinity
in a way
Code
Java
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
if (k >= prices.length / 2) {
return quickSolve(prices);
}
int[][] dp = new int[k + 1][prices.length];
for (int t = 1; t <= k; t++) {
int max = Integer.MIN_VALUE;
for (int j = 1; j < prices.length; j++) {
dp[i][j] = Math.max(dp[i][j - 1], prices[j] + max);
tmpMax = Math.max(tmpMax, dp[i - 1][j] - prices[j]);
}
}
return dp[k][prices.length - 1];
}
// BestTimeToBuyAndSellStock2 - maxprofit
private int quickSolve(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1])
maxProfit += prices[i] - prices[i - 1];
}
return maxProfit;
}
Complexity
- ⏰ Time complexity:
O(k.n)
- 🧺 Space complexity:
O(k.n)
Space complexity can be optimized to O(n)
by utilizing the results from the previous transaction.
Method 5 - 1D Dynamic Programming
The solution above can be simplified to be the following:
Code
Java
public int maxProfit(int k, int[] prices) {
if (prices.length<2 || k<= 0)
return 0;
int[] local = new int[k + 1];
int[] global = new int[k + 1];
for (int i = 0; i<prices.length - 1; i++) {
int diff = prices[i + 1] - prices[i];
for (int j = k; j >= 1; j--) {
local[j] = Math.max(global[j - 1] + Math.max(diff, 0), local[j] + diff);
global[j] = Math.max(local[j], global[j]);
}
}
return global[k];
}
Complexity
- ⏰ Time complexity:
O(k.n)
- 🧺 Space complexity:
O(k)
Method 6 - DP with State Machine
The idea is to design a state machine that accurately represents the problem. There are three possible actions:
- Rest: Do nothing
- Buy
- Sell
When no stock has been bought, we are in a rest state. From here, we can either remain resting or buy a stock. If we own stock, we can either rest or sell it.
Here is the state machine for 2 transactions (k=2
):
stateDiagram-v2 direction LR 0 --> 1: BUY 1 --> 2: BUY 2 --> 3: BUY 3 --> 4: BUY 0 --> 0: REST 1 --> 1: REST 2 --> 2: REST 3 --> 3: REST classDef finalState fill:purple class 4 finalState
Understanding the state diagram for k = 2:
We start at state 0
, where we can either rest (do nothing) or buy stock at the current price.
- Resting keeps us in state
0
. - Buying moves us to state
1
and deducts the stock price from our funds.
In state 1
, we can either rest or sell our stock.
- Resting keeps us in state
1
. - Selling earns us money (stock price of the day) and moves us to state
2
.
This completes one transaction. With k-1
transactions remaining, we move to the next steps.
In state 2
, we can again choose to rest or buy more stock.
- Resting keeps us in state
2
. - Buying moves us to state
3
.
In state 3
, the options are once again to rest or make the final sale.
- Resting keeps us in state
3
. - Selling completes our transactions and moves us to the final state
4
.
Translating the state diagram into code
// Assuming we are in state S
// For buying, we spend money; alternatively, we can do nothing
// Doing nothing: S->S
// Buying: X->S, with a cost
S = max(S, X-prices[i])
// For selling a stock, the situation is similar
S = max(S, X+prices[i])
For generic k:
For k = 2
transactions, there are 4 states. Similarly, for k
transactions, there are 2k
possible states.
Code
Java
Code for K = 2
public int maxProfit(int[] prices) {
int INITIAL_AMOUNT = 0;
int s1 = 0;
int s2 = 0;
int s3 = 0;
int s4 = 0;
int p = 0;
for (int price: prices) {
s1 = Math.max(s1, INITIAL_AMOUNT - price);
s2 = Math.max(s2, s1 + price);
s3 = Math.max(s3, s2 - price);
s4 = Math.max(s4, s3 + price);
p = s4 - INITIAL_AMOUNT;
}
return p;
}
Code for Generic K
private int maxProfit(int k, int[] prices) {
int[] stock = new int[k * 2];
Arrays.fill(stock, Integer.MIN_VALUE);
stock[0] = -prices[0];
for (int i = 1; i<prices.length; ++i) {
stock[0] = Math.max(stock[0], -prices[i]);
for (int j = 1; j<2 * k; j += 2) {
stock[j] = Math.max(stock[j], stock[j - 1] + prices[i]);
if (j + 1<2 * k) {
stock[j + 1] = Math.max(stock[j + 1], stock[j] - prices[i]);
}
}
}
return Math.max(0, stock[2 * k - 1]);
}
Complexity
- ⏰ Time complexity:
O(k.n)
- 🧺 Space complexity:
O(k)
Method 7 - Using State Machine and Recursion + Memo
This problem resembles the knapsack problem with three variables: current index, remaining transactions, and whether a stock is already bought. Based on these variables, we have three possible actions:
- Do nothing.
- If a stock is already bought, sell it.
- Buy the current stock and ignore the previously bought stock.
Base condition: if the index is out of range or the transaction limit is reached.
Code
Java
public int maxProfit(int k, int[] prices) {
// Last 2 stands for isBought
int[][][] memo = new int[prices.length][k+1][2];
for(int i=0; i<prices.length; i++)
for(int j=0; j<k+1; j++)
for(int m=0; m<2; m++)
memo[i][j][m] = -1;
return dfs(k, prices, 0, 0);
}
private int dfs(int k, int[] prices, int idx, int isBought)
{
if(idx >= prices.length || tran == 0)
return 0;
// DP
int bought = isBought == true ? 1 : 0;
if(memo[idx][k][bought] != -1)
return memo[idx][k][bought];
// Do nothing
int profit = helper(k, prices, idx+1, isBought);
if(isBought) // Sell share
profit = Math.max(profit, helper(k-1, prices, idx+1, false) + prices[idx]);
// Buy share
profit = Math.max(profit, dfs(k, prices, idx+1, true) - prices[idx]);
memo[idx][k][bought] = profit;
return profit;
}
Method 8 - State Machine and DP Tabulation
Code
Java
public int maxProfit(int k, int[] price) {
int n = price.length;
int dp[][][] = new int[n + 1][2][k+1];
for (int i = 0; i<n; i++)
for (int j = 0; j<2; j++)
for (int k = 0; k<3; k++)
dp[i][j][k] = 0;
//Tabulation
for (int ind = n - 1; ind >= 0; ind--) {
for (int buy = 0; buy<= 1; buy++) {
for (int cap = 1; cap<= k; cap++) {
if (buy == 1) {
dp[ind][buy][cap] = Math.max(-price[ind] + dp[ind + 1][0][cap], 0 + dp[ind + 1][1][cap]);
} else {
dp[ind][buy][cap] = Math.max(price[ind] + dp[ind + 1][1][cap - 1], 0 + dp[ind + 1][0][cap]);
}
}
}
}
return dp[0][1][k];
}