3573. Best Time to Buy and Sell Stock V
Problem Description
You are given an array prices
where prices[i]
represents the stock price on day i
, and an integer k
representing the maximum number of transactions you can make.
You can perform two types of transactions:
-
Normal transaction: Buy stock on day
i
and sell it on a later dayj
(wherei < j
). Your profit isprices[j] - prices[i]
. -
Short selling transaction: Sell stock on day
i
(without owning it first) and buy it back on a later dayj
(wherei < j
). Your profit isprices[i] - prices[j]
.
The key constraints are:
- You can make at most
k
transactions total - You must complete one transaction before starting another (no overlapping transactions)
- You cannot buy or sell on the same day that you're completing a previous transaction (selling or buying back)
The goal is to find the maximum total profit you can achieve with at most k
transactions.
For example, if prices = [3, 2, 6, 5, 0, 3]
and k = 2
:
- You could buy at price 2, sell at price 6 (profit = 4)
- Then short sell at price 5, buy back at price 0 (profit = 5)
- Total profit = 9
The problem uses dynamic programming with three states at each point:
- State 0: Not holding any position (neither long nor short)
- State 1: Holding a long position (bought stock)
- State 2: Holding a short position (sold stock without owning)
The solution tracks f[i][j][k]
representing the maximum profit on day i
with at most j
transactions in state k
, and uses state transitions to compute the optimal profit.
Intuition
The key insight is recognizing that at any given moment, we can be in one of three distinct states: holding nothing, holding a long position (bought stock), or holding a short position (sold stock we don't own).
Think about it this way - when we're trying to maximize profit, we need to track what position we currently hold because our next action depends on it. If we're holding a stock, we can only sell it. If we're holding a short position, we can only buy back. If we're holding nothing, we can either buy (start a long position) or sell (start a short position).
This naturally leads to a dynamic programming approach where we track three states:
- State 0: Neutral (no position)
- State 1: Long (holding stock)
- State 2: Short (owing stock)
For each day and each possible number of transactions used, we want to know: "What's the maximum profit I can have if I'm in each of these states?"
The transitions between states follow logical rules:
- To be neutral today, I could have been neutral yesterday, or I closed a position today (sold my long or bought back my short)
- To be long today, I either was already long yesterday, or I was neutral yesterday and bought today (this starts a new transaction)
- To be short today, I either was already short yesterday, or I was neutral yesterday and sold today (this also starts a new transaction)
The brilliant part is that starting a new transaction (going from neutral to either long or short) consumes one of our k
allowed transactions. This is why when we transition from state 0 to state 1 or 2, we use f[i-1][j-1][0]
- we're using up one transaction count.
By building up this table day by day and transaction by transaction, we eventually find the maximum profit possible with at most k
transactions, which will be when we end in the neutral state (we don't want to be holding any position at the end).
Learn more about Dynamic Programming patterns.
Solution Approach
We implement a 3D dynamic programming solution where f[i][j][k]
represents the maximum profit on day i
with at most j
transactions in state k
.
State Definition:
k = 0
: Not holding any position (neutral)k = 1
: Holding a long position (bought stock)k = 2
: Holding a short position (sold stock without owning)
Initialization:
We create a 3D array f
with dimensions [n][k+1][3]
where n
is the number of days. For day 0, we initialize:
f[0][j][1] = -prices[0]
for allj
from 1 tok
(buying stock on day 0)f[0][j][2] = prices[0]
for allj
from 1 tok
(short selling on day 0)f[0][j][0] = 0
(implicitly, as the array is initialized with zeros)
State Transitions:
For each day i
from 1 to n-1
and each transaction count j
from 1 to k
, we update the states:
-
Neutral State (k=0):
f[i][j][0] = max( f[i-1][j][0], # Stay neutral f[i-1][j][1] + prices[i], # Sell long position f[i-1][j][2] - prices[i] # Buy back short position )
We take the maximum profit from three scenarios: remaining neutral, closing a long position (selling at today's price), or closing a short position (buying back at today's price).
-
Long State (k=1):
f[i][j][1] = max( f[i-1][j][1], # Stay long f[i-1][j-1][0] - prices[i] # Buy stock (new transaction) )
Either we were already long, or we start a new transaction by buying stock. Note that buying uses
j-1
transactions since we're starting a new one. -
Short State (k=2):
f[i][j][2] = max( f[i-1][j][2], # Stay short f[i-1][j-1][0] + prices[i] # Sell stock (new transaction) )
Either we were already short, or we start a new transaction by short selling. This also uses
j-1
transactions.
Final Result:
After processing all days and transactions, we return f[n-1][k][0]
, which represents the maximum profit after at most k
transactions, ending in a neutral position (not holding any stock or short position).
The time complexity is O(n * k)
where n
is the number of days and k
is the maximum number of transactions. The space complexity is O(n * k)
for the 3D DP array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with prices = [3, 2, 6, 5]
and k = 2
(maximum 2 transactions).
We'll build our DP table f[i][j][state]
where:
i
= day (0 to 3)j
= number of transactions used (0 to 2)state
= 0 (neutral), 1 (long), or 2 (short)
Day 0 (price = 3):
- With 0 transactions: All states = 0 (can't do anything)
- With 1 transaction available:
f[0][1][0] = 0
(neutral, no action taken)f[0][1][1] = -3
(bought at 3, profit = -3)f[0][1][2] = 3
(short sold at 3, profit = 3)
- With 2 transactions available: Same as 1 transaction (can only use one on day 0)
Day 1 (price = 2):
- With 1 transaction:
f[1][1][0] = max(0, -3+2, 3-2) = 1
(best: close short from day 0)f[1][1][1] = max(-3, 0-2) = -2
(buy at better price today)f[1][1][2] = max(3, 0+2) = 3
(keep short from day 0)
- With 2 transactions:
f[1][2][0] = max(0, -3+2, 3-2) = 1
f[1][2][1] = max(-3, 1-2) = -1
(can use profit from first transaction)f[1][2][2] = max(3, 1+2) = 3
Day 2 (price = 6):
- With 1 transaction:
f[2][1][0] = max(1, -2+6, 3-6) = 4
(sell long position for profit)f[2][1][1] = max(-2, 0-6) = -2
f[2][1][2] = max(3, 0+6) = 6
(short sell at high price)
- With 2 transactions:
f[2][2][0] = max(1, -1+6, 3-6) = 5
(sell long from better entry)f[2][2][1] = max(-1, 4-6) = -1
f[2][2][2] = max(3, 4+6) = 10
(short after completing first transaction)
Day 3 (price = 5):
- With 2 transactions:
f[3][2][0] = max(5, -1+5, 10-5) = 5
- Final answer:
f[3][2][0] = 5
The maximum profit is 5, achieved by:
- Short selling at 3, buying back at 2 (profit = 1)
- Buying at 2, selling at 6 (profit = 4) Total: 1 + 4 = 5
Solution Implementation
1class Solution:
2 def maximumProfit(self, prices: List[int], k: int) -> int:
3 n = len(prices)
4
5 # dp[i][j][state] represents the maximum profit at day i with at most j transactions
6 # state 0: no stock held
7 # state 1: holding a bought stock (long position)
8 # state 2: holding a sold stock (short position)
9 dp = [[[0] * 3 for _ in range(k + 1)] for _ in range(n)]
10
11 # Initialize first day states for all transaction counts
12 for transaction_count in range(1, k + 1):
13 # Buying stock on day 0 costs prices[0]
14 dp[0][transaction_count][1] = -prices[0]
15 # Short selling on day 0 gains prices[0]
16 dp[0][transaction_count][2] = prices[0]
17
18 # Fill the dp table for each day
19 for day in range(1, n):
20 for transaction_count in range(1, k + 1):
21 # State 0: No stock held
22 # Can come from: staying without stock, selling bought stock, or buying back shorted stock
23 dp[day][transaction_count][0] = max(
24 dp[day - 1][transaction_count][0], # Stay without stock
25 dp[day - 1][transaction_count][1] + prices[day], # Sell bought stock
26 dp[day - 1][transaction_count][2] - prices[day] # Buy back shorted stock
27 )
28
29 # State 1: Holding bought stock (long position)
30 # Can come from: keeping bought stock or buying new stock (uses a transaction)
31 dp[day][transaction_count][1] = max(
32 dp[day - 1][transaction_count][1], # Keep holding
33 dp[day - 1][transaction_count - 1][0] - prices[day] # Buy stock
34 )
35
36 # State 2: Holding shorted stock (short position)
37 # Can come from: keeping shorted stock or shorting new stock (uses a transaction)
38 dp[day][transaction_count][2] = max(
39 dp[day - 1][transaction_count][2], # Keep short position
40 dp[day - 1][transaction_count - 1][0] + prices[day] # Short sell stock
41 )
42
43 # Return maximum profit on last day with no stock held
44 return dp[n - 1][k][0]
45
1class Solution {
2 public long maximumProfit(int[] prices, int k) {
3 int n = prices.length;
4
5 // dp[day][transactionCount][state]
6 // state: 0 = no stock, 1 = holding stock (bought), 2 = holding stock (shorted)
7 long[][][] dp = new long[n][k + 1][3];
8
9 // Initialize first day states for all transaction counts
10 for (int txn = 1; txn <= k; txn++) {
11 // Buying stock on day 0
12 dp[0][txn][1] = -prices[0];
13 // Shorting stock on day 0
14 dp[0][txn][2] = prices[0];
15 }
16
17 // Fill the DP table for each day and transaction count
18 for (int day = 1; day < n; day++) {
19 for (int txn = 1; txn <= k; txn++) {
20 // State 0: Not holding any stock
21 // Can transition from:
22 // - Previous day with no stock (do nothing)
23 // - Previous day holding bought stock (sell it)
24 // - Previous day holding shorted stock (buy to cover)
25 dp[day][txn][0] = Math.max(
26 dp[day - 1][txn][0],
27 Math.max(
28 dp[day - 1][txn][1] + prices[day], // Sell bought stock
29 dp[day - 1][txn][2] - prices[day] // Cover shorted stock
30 )
31 );
32
33 // State 1: Holding stock (bought position)
34 // Can transition from:
35 // - Previous day already holding bought stock (do nothing)
36 // - Previous day with no stock, use a new transaction to buy
37 dp[day][txn][1] = Math.max(
38 dp[day - 1][txn][1],
39 dp[day - 1][txn - 1][0] - prices[day] // Buy stock
40 );
41
42 // State 2: Holding stock (shorted position)
43 // Can transition from:
44 // - Previous day already holding shorted stock (do nothing)
45 // - Previous day with no stock, use a new transaction to short
46 dp[day][txn][2] = Math.max(
47 dp[day - 1][txn][2],
48 dp[day - 1][txn - 1][0] + prices[day] // Short stock
49 );
50 }
51 }
52
53 // Return maximum profit with at most k transactions, ending with no stock
54 return dp[n - 1][k][0];
55 }
56}
57
1class Solution {
2public:
3 long long maximumProfit(vector<int>& prices, int k) {
4 int n = prices.size();
5
6 // Dynamic programming array
7 // dp[day][transactions][state]
8 // state 0: no stock held
9 // state 1: holding stock (bought)
10 // state 2: sold stock (with profit tracking)
11 long long dp[n][k + 1][3];
12 memset(dp, 0, sizeof(dp));
13
14 // Initialize first day states for all transaction counts
15 for (int j = 1; j <= k; ++j) {
16 // Buying stock on day 0 costs prices[0]
17 dp[0][j][1] = -prices[0];
18 // This state represents selling, initialized to prices[0]
19 dp[0][j][2] = prices[0];
20 }
21
22 // Fill the DP table for each day
23 for (int i = 1; i < n; ++i) {
24 for (int j = 1; j <= k; ++j) {
25 // State 0: Not holding stock
26 // Can come from: staying without stock, selling from state 1, or buying from state 2
27 dp[i][j][0] = max({dp[i - 1][j][0],
28 dp[i - 1][j][1] + prices[i],
29 dp[i - 1][j][2] - prices[i]});
30
31 // State 1: Holding stock
32 // Can come from: keeping the stock, or buying (using one transaction)
33 dp[i][j][1] = max(dp[i - 1][j][1],
34 dp[i - 1][j - 1][0] - prices[i]);
35
36 // State 2: Special selling state
37 // Can come from: staying in this state, or selling (using one transaction)
38 dp[i][j][2] = max(dp[i - 1][j][2],
39 dp[i - 1][j - 1][0] + prices[i]);
40 }
41 }
42
43 // Return maximum profit with at most k transactions, ending without stock
44 return dp[n - 1][k][0];
45 }
46};
47
1/**
2 * Calculates the maximum profit from at most k stock transactions
3 * @param prices - Array of stock prices where prices[i] is the price on day i
4 * @param k - Maximum number of transactions allowed
5 * @returns Maximum profit achievable
6 */
7function maximumProfit(prices: number[], k: number): number {
8 const daysCount = prices.length;
9
10 // Initialize 3D DP array
11 // dp[i][j][state] represents the maximum profit on day i with at most j transactions
12 // state: 0 = no stock held, 1 = holding stock (bought), 2 = holding stock (sold)
13 const dp: number[][][] = Array.from({ length: daysCount }, () =>
14 Array.from({ length: k + 1 }, () => Array(3).fill(0))
15 );
16
17 // Initialize first day states for all transaction counts
18 for (let transactionCount = 1; transactionCount <= k; ++transactionCount) {
19 // State 1: Bought stock on day 0 (negative profit)
20 dp[0][transactionCount][1] = -prices[0];
21 // State 2: Sold stock on day 0 (positive profit)
22 dp[0][transactionCount][2] = prices[0];
23 }
24
25 // Fill DP table for remaining days
26 for (let day = 1; day < daysCount; ++day) {
27 for (let transactionCount = 1; transactionCount <= k; ++transactionCount) {
28 // State 0: No stock held
29 // Can come from: previous day no stock, sell from bought state, or buy from sold state
30 dp[day][transactionCount][0] = Math.max(
31 dp[day - 1][transactionCount][0], // Hold no stock
32 dp[day - 1][transactionCount][1] + prices[day], // Sell stock (from bought)
33 dp[day - 1][transactionCount][2] - prices[day] // Buy stock (from sold)
34 );
35
36 // State 1: Holding stock (bought)
37 // Can come from: continue holding or buy stock (uses one transaction)
38 dp[day][transactionCount][1] = Math.max(
39 dp[day - 1][transactionCount][1], // Continue holding
40 dp[day - 1][transactionCount - 1][0] - prices[day] // Buy stock
41 );
42
43 // State 2: Holding stock (sold)
44 // Can come from: continue in sold state or sell stock (uses one transaction)
45 dp[day][transactionCount][2] = Math.max(
46 dp[day - 1][transactionCount][2], // Continue in sold state
47 dp[day - 1][transactionCount - 1][0] + prices[day] // Sell stock
48 );
49 }
50 }
51
52 // Return maximum profit on last day with k transactions and no stock held
53 return dp[daysCount - 1][k][0];
54}
55
Time and Space Complexity
The time complexity is O(n × k)
, where n
is the length of the array prices
and k
is the maximum number of transactions. This is because we have two nested loops: the outer loop iterates through all n
prices, and the inner loop iterates through all k
transactions. For each combination of (i, j)
, we perform constant time operations to update the three states.
The space complexity is O(n × k)
, as we create a 3D array f
with dimensions [n][k+1][3]
. The first dimension has size n
for each day, the second dimension has size k+1
for the number of transactions (from 0 to k), and the third dimension has a fixed size of 3 for the three possible states. Since the third dimension is constant, the overall space complexity simplifies to O(n × k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Transaction Counting
A critical pitfall is misunderstanding when a transaction is counted. Many developers incorrectly assume that both buying and selling count as separate transactions, or that both opening and closing a position count as two transactions.
The Issue: In this problem, a transaction is counted when you open a new position (either buying for long or selling for short), not when you close it. This means:
- Buying stock (opening long) uses one transaction
- Selling that stock (closing long) doesn't use an additional transaction
- Short selling (opening short) uses one transaction
- Buying back (closing short) doesn't use an additional transaction
Incorrect Implementation:
# WRONG: Counting transaction when closing positions
dp[day][transaction_count][0] = max(
dp[day - 1][transaction_count][0],
dp[day - 1][transaction_count - 1][1] + prices[day], # Wrong! Selling shouldn't use a transaction
dp[day - 1][transaction_count - 1][2] - prices[day] # Wrong! Buying back shouldn't use a transaction
)
Correct Implementation:
# CORRECT: Only count transactions when opening positions
dp[day][transaction_count][0] = max(
dp[day - 1][transaction_count][0],
dp[day - 1][transaction_count][1] + prices[day], # Closing long - no transaction used
dp[day - 1][transaction_count][2] - prices[day] # Closing short - no transaction used
)
dp[day][transaction_count][1] = max(
dp[day - 1][transaction_count][1],
dp[day - 1][transaction_count - 1][0] - prices[day] # Opening long - uses a transaction
)
dp[day][transaction_count][2] = max(
dp[day - 1][transaction_count][2],
dp[day - 1][transaction_count - 1][0] + prices[day] # Opening short - uses a transaction
)
2. Array Index Out of Bounds
When accessing dp[day - 1][transaction_count - 1][0]
for buying or short selling, if transaction_count
is 0, this will cause an index error or incorrect behavior.
The Issue:
The loop should start from transaction_count = 1
, not 0, since you need at least 1 transaction to make any trades.
Solution: Always ensure the transaction loop starts from 1:
for transaction_count in range(1, k + 1): # Start from 1, not 0
# ... state transitions
3. Forgetting Edge Cases
Not handling edge cases like k = 0
(no transactions allowed) or n = 0
(empty price array).
Solution: Add edge case checks at the beginning:
def maximumProfit(self, prices: List[int], k: int) -> int:
if not prices or k == 0:
return 0
n = len(prices)
# ... rest of the code
How many times is a tree node visited in a depth first search?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!