Problem overview
The function maxProfit(prices, k) receives an IntArray where prices[i] is the price of a stock on one day, and an integer k. It returns the largest total profit from at most k transactions, where a transaction is one buy followed by one sell. You can't hold two shares at once: a sale must happen before the next purchase.
With prices = [2, 4, 1] and k = 2, the answer is 2: buy at 2 and sell at 4. With prices = [3, 2, 6, 5, 0, 3] and k = 2, the answer is 7: buy at 2 and sell at 6 for 4, then buy at 0 and sell at 3 for 3.
Approach: best profit for each number of trades and days
Trying every set of buy and sell days grows exponentially. Dynamic programming fixes that by remembering answers to smaller questions. Let dp[t][i] be the best profit using at most t transactions in the first i days. Row 0 (no trades) and column 0 (no days) are both 0, and the table starts out that way.
To fill dp[t][i], consider the last day, day i:
- No sale on day
i. Then the answer is no better than what the firsti - 1days gave,dp[t][i - 1]. Using one transaction fewer,dp[t - 1][i], is also a candidate, since "at most" means spare transactions can go unused. - A sale on day
i. The stock was bought on some earlier dayj. That transaction earnsprices[i - 1] - prices[j - 1], and before dayjthere were at mostt - 1transactions, worthdp[t - 1][j - 1].
The table entry is the best of all these options. The answer is dp[k][n], where n is the number of days. Trying every buy day j for every cell is a third nested loop. A running maximum can replace that loop, as the faster version of this problem shows; here it stays because it shows the idea most directly.
Step by step
- Store
prices.sizeinn. It is used repeatedly, for the table and the loops. - Create
dpwithk + 1rows andn + 1columns, all zeros. The extra row and column are the "no trades" and "no days" cases. - Loop
tfrom 1 tok. Row 0 stays at zero. - Loop
ifrom 1 ton. Column 0 stays at zero. Day numbers start at 1 in the table, butpricesis indexed from 0, so dayihas priceprices[i - 1]. - Start from the no-sale options. Set
dp[t][i]to the larger ofdp[t][i - 1]anddp[t - 1][i]. - Try every buy day
jbefore dayi, from 1 up toi - 1. Buying must come before selling. - Compute the profit of buying on day
jand selling on dayi. - Keep the better value. Compare
dp[t - 1][j - 1] + profitwith the currentdp[t][i]. The first part is the best result for the days before the purchase, with one fewer transaction. - Return
dp[k][n].
The solution in Kotlin
class Solution {
fun maxProfit(prices: IntArray, k: Int): Int {
val n = prices.size
val dp = Array(k + 1) { IntArray(n + 1) }
for (t in 1..k) {
for (i in 1..n) {
dp[t][i] = maxOf(dp[t][i - 1], dp[t - 1][i])
for (j in 1 until i) {
val profit = prices[i - 1] - prices[j - 1]
dp[t][i] = maxOf(dp[t][i], dp[t - 1][j - 1] + profit)
}
}
}
return dp[k][n]
}
}
The Kotlin details worth a closer look:
- The table.
Array(k + 1) { IntArray(n + 1) }calls the lambda once per row, so each row is a separateIntArrayof zeros. Writingdp[t][i] = ...changes one cell of one row. - Ranges.
1..kand1..ninclude the upper end, which is the last transaction count and the last day.1 until iexcludesi, so the buy day is always strictly earlier than the sell day. maxOf. It returns the larger of two values, and a call with twoIntarguments gives anInt.- Assigning inside the inner loop.
dp[t][i]is updated repeatedly, once for eachj. It always keeps the best value seen so far, so the final value is the maximum over all options.
Complexity
Time: O(k * n²). There are k * n cells, and each one runs a loop over up to n buy days. A running maximum, described above, brings it down to O(k * n).
Space: O(k * n). The table has (k + 1) * (n + 1) cells. A very large k makes the table large even when n is tiny, because the code allocates all k + 1 rows.
Edge cases and common mistakes
- Falling prices. For
[5, 4, 3, 1]withk = 3the result is 0, because no transaction earns anything and trades can stay unused. k = 0. The loop overtnever runs, anddp[0][n]is 0.- Empty prices. With no days,
dp[k][0]is 0 and the function returns 0. - More transactions than useful. For
[3, 2, 6, 5, 0, 3]withk = 5the answer is still 7, the same as withk = 2. Spare transactions simply go unused. - Off-by-one in prices. Using
prices[i]instead ofprices[i - 1]reads one day too far and throwsArrayIndexOutOfBoundsExceptionon the last day. - Letting
jreachi. That would model a buy and sell on the same day, which earns 0. It is harmless here, but the spec says to buy first. - Using
dp[t - 1][j]for the part before the purchase. Dayjis the buy day, so the earlier transactions must fit in days 1 toj - 1.
Testing
prices = [2, 4, 1],k = 2gives2.prices = [3, 2, 6, 5, 0, 3],k = 2gives7.prices = [3, 2, 6, 5, 0, 3],k = 1gives4.prices = [5, 4, 3, 1],k = 3gives0.prices = [],k = 2gives0.