Best Time to Buy and Sell Stock IV

Problem

Given an integer k and an array prices where prices[i] is the price of a stock on day i, return the maximum profit achievable with at most k transactions, where each transaction consists of buying and then selling one share of the stock.

  • 0 ≤ k ≤ 100
  • 0 ≤ prices.length ≤ 1000
  • 0 ≤ prices[i] ≤ 1000

Example

Input: k = 2, prices = [3,2,6,5,0,3]
Output: 7

The optimal strategy is to buy on day 1 (price = 2) and sell on day 2 (price = 6), profit = 4, then buy on day 4 (price = 0) and sell on day 5 (price = 3), profit = 3, for a total profit of 7. The algorithm explores states defined by day index, holding status, and remaining transactions. It recursively decides whether to buy, sell, or skip each day, caching results to avoid redundant calculations.

Approach

Straightforward Solution

A brute-force approach tries all possible sequences of buy and sell actions, leading to exponential time complexity due to combinatorial explosion of choices. Without memoization, exploring all buy/sell sequences leads to O(3^(n)) behavior due to branching choices at each day.

Core Observation

The problem requires maximizing profit with at most k buy-sell pairs, where each decision depends on whether a stock is currently held and how many transactions remain. This naturally forms a stateful decision process with overlapping subproblems.

Path to Optimal

Recognizing the problem's optimal substructure and overlapping subproblems suggests Dynamic Programming. Modeling the problem as a state machine with parameters (day, holding status, remaining transactions) allows recursive exploration with memoization. The key insight is that each state can be solved optimally by considering the best among skipping, buying, or selling actions, and caching results prevents redundant work.

Optimal Approach

Use a top-down DP with memoization. Define a recursive function dp(i, holding, remain) that returns the maximum profit achievable starting from day i, with holding indicating if a stock is currently held, and remain the number of transactions left. At each state, compute the max profit by choosing to skip, buy (if not holding), or sell (if holding), and memoize the result. The answer is dp(0, False, k).

Time

O(n * k * 2)

There are n days, k possible remaining transactions, and 2 holding states, resulting in O(n*k*2) unique states. Each state is computed once and memoized, so total time is O(n*k).

Space

O(n * k * 2)

The memoization dictionary stores results for each unique state tuple (day, holding, remaining transactions), leading to O(n*k) auxiliary space. This excludes input and output space.

Pattern Spotlight

Dynamic Programming (State Machine)

When a problem involves sequential decisions with limited states (like holding/not holding and transaction counts), model it as a state machine and use DP to explore all valid paths efficiently by caching overlapping subproblems.

Solution

1class Solution:
2 def maxProfit(self, k: int, prices: List[int]) -> int:
3 memo = {}
4
5 def dp(i, holding, remain):
6 if i == len(prices) or remain == 0:
7 return 0
8
9 key = (i, holding, remain)
10 if key in memo:
11 return memo[key]
12
13 ans = dp(i + 1, holding, remain)
14
15 if holding:
16 ans = max(ans, prices[i] + dp(i + 1, False, remain - 1))
17 else:
18 ans = max(ans, -prices[i] + dp(i + 1, True, remain))
19
20 memo[key] = ans
21 return ans
22
23 return dp(0, False, k)

Step-by-Step Solution

1

Cache Computed States to Avoid Redundant Computation

3memo = {}

Objective

To store results of subproblems defined by day, holding status, and remaining transactions to prevent exponential recomputation.

Key Insight

Memoization transforms the naive exponential recursion into polynomial time by caching the results of each unique state. Since the problem's state space is defined by three parameters, caching results keyed by these parameters ensures each state is solved once, enabling efficient reuse.

Interview Quick-Check

Core Logic

Memoization uses a dictionary keyed by (day, holding, remaining transactions) to store and retrieve computed results, preventing redundant recursive calls.

Common Pitfalls & Bugs

Failing to memoize leads to exponential time complexity due to repeated exploration of identical states.

2

Recursively Explore All Trading Decisions with State Parameters

5def dp(i, holding, remain):
6 if i == len(prices) or remain == 0:
7 return 0
9 key = (i, holding, remain)
10 if key in memo:
11 return memo[key]
13 ans = dp(i + 1, holding, remain)
15 if holding:
16 ans = max(ans, prices[i] + dp(i + 1, False, remain - 1))
17 else:
18 ans = max(ans, -prices[i] + dp(i + 1, True, remain))
20 memo[key] = ans
21 return ans

Objective

To compute the maximum profit from day i onward, considering whether currently holding stock and how many transactions remain.

Key Insight

The recursive function models the problem as a state machine with three parameters. At each state, it considers skipping the day, buying if not holding, or selling if holding. This exhaustive exploration ensures all valid trading sequences are considered. The max operation selects the optimal choice, and memoization guarantees efficiency.

Interview Quick-Check

Core Logic

At each state, the algorithm chooses the best among skipping, buying (if not holding), or selling (if holding), recursively computing future profits and caching results. Memoization check ensures we skip already computed states.

State & Boundaries

Base cases occur when all days are processed or no transactions remain, returning zero profit. The memo ensures base cases aren’t repeatedly recomputed.

Common Pitfalls & Bugs

Incorrectly decrementing transactions on buy instead of sell leads to wrong results. Forgetting to memoize the computed answer causes exponential blowup.

3

Start Dynamic Programming From the Initial State

23return dp(0, False, k)

Objective

Begin the recursive DP from day 0 with an empty portfolio and the full transaction budget.

Key Insight

All profit calculations flow from this single starting state where no stock is held and all k transactions are still available.

Interview Quick-Check

Core Logic

Stock IV DP always begins at day 0 with holding = False and remain = k.

Line Analysis

This solution has 7 Critical lines interviewers watch for.

Line 10 Critical
if key in memo:

Check if the current state has been computed before.

This lookup prevents redundant recursive calls by reusing previously computed results, crucial for reducing time complexity from exponential to polynomial.

Line 16 Critical
ans = max(ans, prices[i] + dp(i + 1, False, remain - 1))

Calculate profit by selling the stock today and decrementing remaining transactions.

Selling realizes profit by adding the current price and consumes one transaction, transitioning to not holding state for future decisions.

Line 18 Critical
ans = max(ans, -prices[i] + dp(i + 1, True, remain))

Calculate profit by buying the stock today without decrementing transactions.

Buying costs the current price and transitions to holding state, but does not reduce transaction count since a transaction completes only upon selling.

Line 3 Critical
memo = {}

Initialize memoization dictionary to cache computed states.

This dictionary is essential to store results of subproblems keyed by state, enabling the DP to avoid redundant computations and achieve polynomial time complexity.

Line 6 Critical
if i == len(prices) or remain == 0:

Check base cases: if all days processed or no transactions remain, return zero profit.

These termination conditions prevent infinite recursion and correctly represent that no further profit can be made beyond these points.

Line 11 Critical
return memo[key]

Return the cached result for the current state if available.

Returning cached results immediately prunes the recursion tree, ensuring each state is solved once and improving efficiency dramatically.

Line 20 Critical
memo[key] = ans

Cache the computed maximum profit for the current state.

Memoizing the result ensures that future calls with the same state return instantly, preventing exponential recomputation.

Line 5 Core
def dp(i, holding, remain):

Define the recursive DP function with parameters for day, holding status, and remaining transactions.

This function encapsulates the entire state space of the problem, allowing systematic exploration of all trading decisions with clear state representation.

Line 9 Core
key = (i, holding, remain)

Create a key tuple representing the current state for memoization.

The key uniquely identifies the current subproblem, enabling efficient caching and retrieval of computed results.

Line 17 Core
else:

Update the answer with the maximum profit between skipping and selling.

Selecting the maximum ensures the algorithm chooses the optimal action at this state, embodying the principle of optimal substructure.

Line 21 Core
return ans

Return the maximum profit computed for the current state.

Returning the cached optimal profit completes the recursive step and propagates the solution up the call stack.

Line 7 Core
return 0

Return zero profit for base cases.

Returning zero here correctly signals that no profit is possible when no days or transactions remain, forming the foundation for recursive profit calculations.

Line 13 Core
ans = dp(i + 1, holding, remain)

Calculate profit by skipping the current day and maintaining the current state.

This option preserves the current holding and transaction count, representing the choice to do nothing and wait for a better opportunity.

Line 15 Core
if holding:

Check if currently holding a stock to decide possible actions.

Holding status determines whether selling or buying is valid, guiding the recursive decision tree.

Line 23 Core
return dp(0, False, k)

Start the DP from the initial state: first day, not holding stock, all transactions available.

This initializes the DP search from day 0 with no stock held and the full number of allowed transactions, launching the recursive exploration of all valid trading decisions.

Test Your Understanding

Why does the transaction count decrement only when selling, not when buying?

Because a transaction is defined as a complete buy-sell pair. Buying initiates a transaction but does not complete it, so the count of remaining transactions only decreases upon selling.

Related Problems

Dynamic Programming pattern

Don't just read it. Drill it.

Reconstruct Best Time to Buy and Sell Stock IV from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Best Time to Buy and Sell Stock IV drill

or drill a free problem