Best Time to Buy and Sell Stock II
Introduction
Same price series as the single-transaction problem, one constraint relaxed: unlimited buy-and-sell pairs are allowed, as long as you never hold more than one share at a time. At first glance this is harder — the search space of “all possible non-overlapping transaction sets” is exponential. The surprise: this version is actually simpler than the single-transaction case. The optimal answer is one of the cleanest one-liners in interview DP.
The interesting interview content isn’t the greedy answer itself — it’s the correctness argument that justifies it, and the state-machine framing that ports cleanly to the harder cousin variants (cooldown, fee, limited transactions). This chapter walks all three: the one-line greedy, the proof that the one-liner is correct, and the DP framing that bridges to the rest of the family.
When a constraint is removed, sometimes the algorithm gets simpler — not more complex. The interview signal is recognizing the moment.
If you’re new to the buy/sell family, read the single-transaction chapter first, then come back. If you’ve coded LC 121, the greedy approach here will feel almost trivial — the value is in the state-machine framing, which is the gateway to the harder variants.
What the interviewer is actually testing
- Recognizing the relaxation. Many candidates over-engineer LC 122 because they pattern-match it to LC 121 and write a more complex DP. The signal is noticing that unlimited trades is easier, not harder.
- Articulating the correctness of the greedy. “Sum of positive deltas” sounds heuristic. Proving it equals the true optimum — by showing any larger trade decomposes into a sum of single-day trades — is the senior signal.
- The state-machine framing. Two states (idle / holding), two transitions (buy / sell). This is the structure the rest of the family generalizes from.
- Edge cases with no profitable trade. Strictly decreasing or constant inputs return 0 cleanly under the greedy formulation. Verifying that’s not a bug is part of the read.
Problem statement
You are given an integer array prices where prices[i] is the price of a stock on day . On any day you may buy or sell, but you can hold at most one share at a time (you must sell before buying again). You may transact as many times as you like. Return the maximum profit achievable.
prices = [7, 1, 5, 3, 6, 4] 7 prices = [1, 2, 3, 4, 5] 4 prices = [7, 6, 4, 3, 1] 0 Clarifying questions — and why each one matters
Clarification questions
- Q Same-day buy-and-sell — allowed?
- A Yes per the standard formulation, with profit 0. The greedy doesn't even consider it because the inner check is on adjacent-day deltas, not same-day.
- Q Is there a transaction fee, or a cooldown between trades?
- A No in this problem; if there were, it would be a different LC variant (714 or 309 respectively). Always confirm to disambiguate which family member the interviewer wants.
- Q Are short positions allowed?
- A No. The problem explicitly forbids holding more than one share, and there's no negative-share state. Short-selling would change the problem fundamentally.
- Q What's the upper bound on n?
- A Typical: n ≤ 3 × 10⁴ for LC 122. Linear time is comfortable; the brute force is hopeless even at n = 30.
- Q Can prices be zero or negative?
- A Standard formulation says non-negative. Even if negatives were allowed, the greedy still works — it only cares about the sign of each delta.
- Q Are we returning the profit, or also the trade list?
- A Default is just the profit. If asked for the trade list, the greedy approach naturally yields one trade per up-day (i-1, i); the more complex DP variants need backtracking through the state machine to reconstruct.
A mental framework — the relaxation makes it easier
LC 121 (single transaction) needed one number from the past — the running minimum — because you had to commit to a single buy day. LC 122 removes that constraint entirely. With unlimited trades, you don’t have to commit to anything: every profitable swing is independent of every other.
That changes the question. Instead of asking “what’s the best single buy/sell pair?”, ask “on each day, did the price go up?”. If yes, take the delta as a one-day trade. If no, skip. The total profit is the sum of all up-day deltas.
This isn’t a heuristic — it’s the optimum. Any longer trade [i, j] with telescopes:
Some of those daily deltas are positive, some negative. The greedy keeps only the positives — which is at least as much profit as the long trade gives, because the greedy can simply skip the down-days that the long trade is forced to absorb.
Discussion
Brute force — try every transaction set
Recursively enumerate every valid set of non-overlapping (buy, sell) pairs, return the maximum total. Exponential, correct, your starting line.
The interview narration
Say this out loud:
“A transaction set is a sequence of non-overlapping (buy, sell) pairs. The brute force enumerates every such set and tracks the maximum total profit. The number of valid sets grows exponentially in — roughly the Catalan-like count of non-crossing pair sets. I’ll write the recursion as a baseline and find something faster.”
How it works, step by step
The recursion takes a starting day start. For each pair (buyDay, sellDay) with buyDay >= start and buyDay < sellDay, if the trade is profitable (prices[sellDay] > prices[buyDay]), recurse on the remaining days [sellDay + 1, n) to find the best follow-up profit, and combine.
The function returns the maximum over all (buyDay, sellDay) choices, or 0 if no profitable trade exists.
The code
#include <algorithm>#include <vector>
namespace {int solve(const std::vector<int>& P, int start) { int best = 0; const int n = static_cast<int>(P.size()); for (int buyDay = start; buyDay < n - 1; ++buyDay) { for (int sellDay = buyDay + 1; sellDay < n; ++sellDay) { if (P[buyDay] < P[sellDay]) { const int profit = P[sellDay] - P[buyDay]; best = std::max(best, profit + solve(P, sellDay + 1)); } } } return best;}} // namespace
int maxProfit(const std::vector<int>& prices) { return solve(prices, 0);}A double loop with a recursive call inside. Each profitable trade spawns its own recursive subtree. Exponential — useful only as the conceptual baseline.
Why it’s rejected
The recursion’s branching factor is the number of profitable pairs starting at the current day, which can be . With recursion depth up to , the total work is exponential. For this is already minutes; for the typical , it’s astronomical.
What it gets right
- Correctness is straightforward. Every transaction set is reachable via some recursion path, so the maximum can’t be missed.
- It exposes the structural redundancy. The recursion solves overlapping subproblems on
[k, n)for many differentkvalues, which suggests memoization. But the next approach skips DP entirely with a clean greedy argument.
Greedy — sum of positive deltas Recommended
Walk left to right. Whenever today's price exceeds yesterday's, add the difference to the total. Skip otherwise. One pass, no state.
The insight, stated cleanly
The unconstrained problem is solved by an unconstrained greedy: take every up-day delta as a one-day trade. The total profit is the sum of all positive day-to-day differences.
This isn’t an approximation. The proof: every multi-day trade [i, j] with positive total profit telescopes into a sum of consecutive day-to-day deltas. The greedy captures all the positive ones and avoids all the negative ones — strictly dominating any single trade that’s forced to include both. Combined with the constraint that we can transact every day, the per-day-delta decomposition is achievable, not just an upper bound.
Start. Walk pairs (i-1, i) from i = 1.
How it works, step by step
For from to :
- Compute
delta = prices[i] - prices[i - 1]. - If
delta > 0, add it to the running total. - Otherwise, skip.
After the loop, the running total is the answer. There is no state beyond a single integer accumulator. There is no decision logic beyond the sign of the delta. The whole algorithm is a one-line summation with a non-negative filter.
The code
#include <vector>
int maxProfit(const std::vector<int>& prices) { int total = 0; for (std::size_t i = 1; i < prices.size(); ++i) { if (prices[i] > prices[i - 1]) { total += prices[i] - prices[i - 1]; } } return total;}Three lines of body. The same template as Maximum Subarray's running-sum, restricted to non-negative deltas.
Correctness, precisely — why “sum of positive deltas” equals the optimum
This is the section interviewers probe. State the argument explicitly.
Claim. Let be the maximum total profit achievable by any sequence of non-overlapping trades. Let be the sum of positive day-to-day deltas. Then .
Proof. (, every trade set is bounded by .) Let be any valid trade set with . Each trade’s profit telescopes:
The total trade profit is:
where is the day-to-day delta. Because trades don’t overlap, each appears in at most one of the inner sums. So the double sum is bounded above by .
(, the greedy strategy is achievable.) Treat each up-day as its own one-day trade (j-1, j). These trades are pairwise non-overlapping (they touch only at endpoints, which is allowed), and they sum to exactly . So is achieved by a valid trade set.
Combined: . ✓
The argument is short enough to write on a whiteboard. State it during the interview — the senior signal isn’t writing the loop, it’s articulating why the loop is correct.
Edge cases the mechanics handle for free
- Strictly decreasing prices: every delta is non-positive; the running total stays at 0. Correct (no profitable trade).
- All same price: every delta is 0; total stays at 0. Correct.
- Single day (): the loop never executes; total is 0. Correct.
- Empty input: same as single day.
- Strictly increasing: every delta is positive; total equals
prices[n-1] - prices[0]. Correct (single buy-then-sell over the whole range, equivalent to summing all deltas).
Complexity
- Time: . One pass, two arithmetic ops per iteration.
- Space: . One integer accumulator.
State-machine DP — the bridge to harder variants
Two running states (idle / holding), updated each day by considering buy or sell. Same O(n) complexity as greedy; less elegant for this problem but the framing extends to LC 309 and LC 714.
The insight
Model the trader’s position as a state machine. At every point the trader is in one of two states: idle (no shares) or holding (one share). Each day, the trader decides to stay in the current state or transition to the other:
idle → holding: buy a share, payingprices[i].holding → idle: sell the share, receivingprices[i].- Self-loops on either state: do nothing.
The DP carries one running value per state — the maximum profit reachable while ending in that state at the current day. The recurrence updates them as we walk:
The answer at the end is idle (we’d rather end without holding a worthless share).
How it works, step by step
Initialize holding = -prices[0] (we’ve bought on day 0, so our position has a “profit”) and idle = 0 (we haven’t done anything).
For each subsequent day i:
- Cache the previous
holdingbefore updating, so theidleupdate can use it. - Update
holding: stay holding (no change), or buy today from the previousidlestate. Take the max. - Update
idle: stay idle (no change), or sell today from the previousholdingstate. Take the max.
Both updates run in . The total is time, space.
The code
#include <algorithm>#include <vector>
int maxProfit(const std::vector<int>& prices) { if (prices.empty()) return 0; int holding = -prices[0]; // best profit if currently holding a share int idle = 0; // best profit if currently not holding for (std::size_t i = 1; i < prices.size(); ++i) { const int prevHolding = holding; holding = std::max(holding, idle - prices[i]); idle = std::max(idle, prevHolding + prices[i]); } return idle;}Two integers carry the per-state profits. The `prevHolding` cache prevents the `idle` update from reading the just-updated `holding` value.
Why bother with this if the greedy already works?
The state-machine framing extends cleanly to every harder buy/sell variant:
- LC 309 (cooldown): add a third state for “just sold, can’t buy yet.” Three running variables, same loop structure.
- LC 714 (fee): subtract the fee from the
holding → idletransition. Two states, one extra subtraction. - LC 123 (at most 2 transactions): four states tracking how many transactions have completed. Same loop structure with four running variables.
- LC 188 (at most k transactions): states, an array instead of named variables. Same loop.
Once you’ve seen this framing, none of those variants are conceptually harder than this one. The greedy doesn’t generalize; the state machine does.
Complexity
- Time: . Same as greedy.
- Space: . Two integers.
Approach comparison
| Approach | Time | Space | When to reach for it |
|---|---|---|---|
| Brute force | exponential | Baseline only; never ship. | |
| Greedy (sum of positive deltas) | Default for this problem. Three lines, provably optimal. | ||
| State-machine DP | When the interviewer is leading toward the harder variants; same complexity, less elegant for LC 122 alone. |
Default answer: the greedy. State the telescoping correctness argument out loud when you close.
Follow-ups to anticipate
| The interviewer asks… | What to do |
|---|---|
| ”Return the actual list of trades, not just the profit.” | The greedy yields one trade per up-day pair. Walk the array and emit (i-1, i) for each delta > 0. Or merge consecutive up-days into a single trade — same total profit. |
| ”Add a $1 transaction fee per sell.” | LC 714. Switch to the state-machine DP and subtract the fee on the holding → idle transition. Greedy breaks because tiny up-day deltas no longer cover the fee. |
| ”Add a 1-day cooldown after each sell.” | LC 309. Three states: holding, cooldown, idle. Transition graph changes; same DP loop. |
| ”At most transactions.” | LC 188. states (one idle and one holding per transaction count). The 2-state idea generalizes; the greedy doesn’t. |
| ”What if shorts are allowed?” | A different problem entirely — now negative-share states exist. The state machine grows; the greedy is no longer correct because a negative delta on a short position is profitable. |
| ”Streaming prices — answer must update each day.” | The greedy is already streaming-friendly. New price arrives, compare with the previous, add the positive delta. |
| ”Why does the greedy work but not on LC 121?” | Because LC 121 forbids multiple transactions. The telescoping argument requires being allowed to take every up-day delta as its own trade; LC 121’s single-trade constraint prevents that. |
| ”How do you prove the greedy without telescoping?” | The state-machine DP is an alternative proof: it computes the optimum over all valid trade sequences, and you can verify by induction that its answer equals the sum of positive deltas. The two approaches arrive at the same conclusion via different routes. |
Pattern recognition — when constraint relaxation simplifies
This chapter teaches a pattern that’s worth naming explicitly: removing a constraint sometimes makes the algorithm simpler, not more complex. The intuition is that a tight constraint (LC 121: one trade) forces global commitment, which requires global state. A loose constraint (LC 122: any number of trades) decouples decisions across time, which lets a local greedy work.
Other problems with the same shape:
- Maximum Subarray vs. Maximum Sum Increasing Subsequence. The first is unconstrained on element selection (any contiguous range), and Kadane’s runs in . Adding the “increasing” constraint forces a more expensive DP.
- Minimum spanning tree (Kruskal’s). Without the spanning constraint, the answer is “no edges.” Adding the constraint forces a non-trivial algorithm. But within Kruskal’s, removing the no-cycle constraint would make every edge selectable — trivially.
- Coin change (with infinite supply) vs. 0/1 knapsack. Infinite supply allows a 1D DP; the 0/1 constraint forces 2D.
The reflex: when an interviewer relaxes a constraint and asks for the new answer, ask whether the relaxation decouples decisions across time or items. If yes, the answer might be a one-line greedy.
What to say out loud — the delivery script
Ten beats:
- “Let me restate: same prices array as the single-transaction problem, but unlimited trades are allowed. Maximize total profit.”
- Ask: “fee or cooldown? upper bound on n? profit or trade list?”
- “Counterintuitively, this is easier than the single-transaction version. The reason: any long trade telescopes into a sum of single-day trades, and we can take every up-day independently.”
- “So the greedy: walk left to right, sum every positive day-to-day delta. That’s the entire algorithm.”
- Write the code — three lines.
- “Correctness: any trade set is bounded above by the sum of positive deltas (telescoping argument). And the sum of positive deltas is achievable by a valid trade set (one trade per up-day). So they’re equal.”
- Trace example 1: deltas are
[-6, +4, -2, +3, -2]. Sum of positives: 4 + 3 = 7. Matches the expected output. - “Edge cases: strictly decreasing prices give 0 (no positive deltas); all-same prices give 0; single day gives 0. All handled by the loop body.”
- “For the harder variants — fees, cooldowns, k-transaction limits — the greedy breaks. The state-machine DP is the framing that survives.”
- “Complexity: time, space. I’d ship the greedy.”
Summary
The unlimited-transaction buy/sell problem is the rare case where relaxing a constraint simplifies the algorithm. Where the single-transaction version needs a running minimum and careful pair tracking, this version reduces to a one-line summation: sum every positive day-to-day delta.
The interview signal isn’t writing the three-line greedy. It’s two things: articulating the telescoping argument that proves the greedy is optimal, and recognizing the state-machine framing as the structural cousin of the harder family members. The greedy is what you ship; the state machine is what you say when the interviewer asks the next question in the family.