Problem overview
The function max_profit receives prices, where prices[d] is a stock's price on a given day, and k, the maximum number of transactions allowed. A transaction is one buy followed by one sell. You can't hold two purchases at once: the stock must be sold before you buy again. The function returns the largest total profit.
In the second example, prices = [3, 2, 6, 5, 0, 3] and k = 2. Buying at 2 and selling at 6 earns 4, and buying at 0 and selling at 3 earns 3. The total is 7. In the first example, [2, 4, 1], one transaction (buy at 2, sell at 4) earns 2, and a second one can't add anything.
Approach: best profit for each number of transactions and days
With one transaction the task is to pick the lowest earlier price for each sale. With several transactions, the choices interact, so a table keeps them organised. Let dp[t][i] be the best profit using at most t transactions during the first i days. Row 0 (no transactions) and column 0 (no days) are always 0.
To compute dp[t][i], consider what happens on day i:
- No sale on day
i. The answer is no worse thandp[t][i - 1], the same budget with one day less. It is also no worse thandp[t - 1][i], because "at most"ttransactions includes using fewer. - A sale on day
i. Choose the dayjbeforeion which that stock was bought. The profit of the last transaction isprices[i] - prices[j], and before dayjyou used at mostt - 1transactions, worthdp[t - 1][j - 1]. Try everyjand keep the best.
This is the straightforward version of the idea: for every cell it scans all possible buy days. A faster version that removes that scan exists, and the next lesson on this problem covers it.
Step by step
- Store the length of
pricesinn, because it is used constantly, and rebindkas ausizeso it can size the table. - Create the table.
dpis a mutable vector ofk + 1rows, each withn + 1zeros of typei32. - Loop over
tfrom 1 tok. Row 0 stays 0, so the loop skips it. - Loop over
ifrom 1 ton. Column 0 also stays 0. - Set the starting value for the "no sale on day
i" case: the larger ofdp[t][i - 1]anddp[t - 1][i], usingmax. - Loop over the buy day
jfrom 1 toi - 1. Buying has to happen before selling. - Compute the profit of buying on day
jand selling on dayi. Days are numbered from 1 here butpricesis indexed from 0, so the prices areprices[j - 1]andprices[i - 1]. - Update the cell if this last transaction, added to the best profit before day
jwith one transaction fewer, beats what is stored. - Return
dp[k][n]as the last expression.
The solution in Rust
impl Solution {
fn max_profit(prices: Vec<i32>, k: i32) -> i32 {
let n = prices.len();
let k = k as usize;
let mut dp = vec![vec![0i32; n + 1]; k + 1];
for t in 1..=k {
for i in 1..=n {
dp[t][i] = dp[t][i - 1].max(dp[t - 1][i]);
for j in 1..i {
let profit = prices[i - 1] - prices[j - 1];
dp[t][i] = dp[t][i].max(dp[t - 1][j - 1] + profit);
}
}
}
dp[k][n]
}
}
The Rust details worth knowing:
- Methods on numbers.
a.max(b)returns the larger of two integers. It is a method of the number type, sodp[t][i - 1].max(dp[t - 1][i])reads left to right without importing anything. - Copying out of the vector. Indexing a
Vec<i32>gives ani32, which isCopy. That is whydp[t][i] = dp[t][i].max(...)reads and writes the same cell without any borrowing trouble. - Ranges.
1..=kand1..=ninclude their upper end.1..iexcludesi, sojstops ati - 1, exactly the days before the sale. - Two sets of indexes. The table counts days from 1 and
pricescounts from 0. The- 1inprices[i - 1]converts between them, anddp[t - 1][j - 1]means "the firstj - 1days". - Row cloning. The nested
vec!makesk + 1independent rows. The literal0i32makes the cells the same type as the prices. - Overflow. The profit is a difference of two prices, and sums of such differences stay within an
i32for realistic price lists. In the optimized builds the lessons use, an overflow would wrap silently rather than stop the program.
Complexity
Time: O(k * n²). The loops over t, i and j are nested. The inner loop runs up to n times for each of the k * n cells.
Space: O(k * n). The table holds (k + 1) * (n + 1) numbers.
The cubic cost is what the faster lesson improves: it keeps a running best value for the buy day, so the inner loop disappears and the time drops to O(k * n).
Edge cases and common mistakes
- Prices that only fall.
[5, 4, 3]withk = 2gives 0. Every profit is negative, and the starting value from the no-sale case keeps the cell at 0. - No transactions. With
k = 0the loops overtdon't run anddp[0][n]is 0. - Empty or single-day input. With no prices or one price, no sale is possible and the result is 0. The inner loop is empty, and
n = 0reads onlydp[k][0]. - More transactions than needed. For
[7, 1, 5, 3, 6, 4]andk = 1the answer is 5 (buy at 1, sell at 6). Extra transactions never reduce the result, because "at most" allows using fewer. - Ignoring the "at most" rule. Without the
dp[t - 1][i]term, a budget oftwould be forced to use allttransactions. - Reusing a day. The previous profit comes from
dp[t - 1][j - 1], the days before the buy day. Usingdp[t - 1][j]would let one day serve as both a sale and a purchase. - Index off by one. Writing
prices[i]instead ofprices[i - 1]reads past the end on daynand panics.
Testing
prices = [2, 4, 1],k = 2gives2.prices = [3, 2, 6, 5, 0, 3],k = 2gives7.prices = [7, 1, 5, 3, 6, 4],k = 1gives5.prices = [1, 5, 2, 8, 3, 9],k = 3gives16.prices = [5, 4, 3],k = 2gives0.
