Problem overview
The method maxProfit(prices, k) receives the price of a stock on each day and a limit k on the number of transactions. A transaction is a purchase followed by a sale, and you can hold only one share at a time. The method returns the largest total profit, and 0 when no trade makes money.
For 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. For prices = [2, 4, 1] and k = 2 the answer is 2, from a single trade.
Approach: remove the inner loop
The table is the same as in the slower solution to this problem. dp(t)(i) is the best profit using at most t transactions in the first i days. That solution tries every buy day j for every cell, which costs O(k * n^2) in total.
The inner loop is a search for the best buy day. Selling on day i gives prices(i - 1) plus the best value of "profit before buying, minus the price paid", taken over the earlier days. That second part doesn't depend on i at all, apart from which days have been seen so far. So it can be kept as a running maximum:
maxDiff = the largest dp(t - 1)(j) - prices(j - 1) over the days j seen so far
Each cell then costs a constant amount of work: dp(t)(i) is the best of three options.
dp(t)(i - 1): no sale on dayi.dp(t - 1)(i): use one transaction fewer.maxDiff + prices(i - 1): sell on dayia share bought on the best earlier day.
The running value is updated after the cell is computed, with day i's own candidate dp(t - 1)(i) - prices(i - 1). That order matters: a share bought on day i can only be sold on a later day.
Here is row t = 2 for prices = [3, 2, 6, 5, 0, 3]. Row 1 is 0, 0, 0, 4, 4, 4, 4 for 0 to 6 days. In row 2, maxDiff stays between -3 and -1 through day 4, and the cells for days 1 to 4 are 0, 0, 4, 4. On day 5 the cell is still 4, but then maxDiff becomes dp(1)(5) - 0 = 4: row 1 says 4 can be earned in the first five days, and the share costs 0. On day 6 the sale at 3 gives 4 + 3 = 7.
Step by step
- Store
prices.lengthinn. - Create
dpas anArray.ofDim[Int](k + 1, n + 1)of zeros. Row 0 and column 0 stay 0. - Loop over the transaction count
tfrom 1 tok. - Declare
maxDiffwithvar, inside that loop, so each row starts fresh. Its starting value isInt.MinValue: no day has been seen yet, so there is no valid buy, and any real candidate must win against it. - Loop over the days
ifrom 1 ton. - Compute
dp(t)(i)as the best of the three options, nesting onemath.maxinside another. - Update
maxDiffwithdp(t - 1)(i) - prices(i - 1), keeping the larger value. - Return
dp(k)(n).
The solution in Scala
class Solution {
def maxProfit(prices: Array[Int], k: Int): Int = {
val n = prices.length
val dp = Array.ofDim[Int](k + 1, n + 1)
for (t <- 1 to k) {
var maxDiff = Int.MinValue
for (i <- 1 to n) {
dp(t)(i) = math.max(math.max(dp(t)(i - 1), dp(t - 1)(i)), maxDiff + prices(i - 1))
maxDiff = math.max(maxDiff, dp(t - 1)(i) - prices(i - 1))
}
}
return dp(k)(n)
}
}
Some Scala details in this code:
varagainstval.maxDiffchanges on every day, so it must be avar.dpis aval: the name is fixed, though the cells inside can still be assigned.Int.MinValueis -2147483648, the smallestInt. It makes a good "nothing yet" starting value for a maximum. It is risky to add to, becauseIntoverflow wraps silently in Scala. Here the first addition isInt.MinValue + prices(0), and a non-negative price only moves the value up, so it can't wrap. A negative price would wrap to a huge positive number.- Why not
0as the starting value? The differencedp(t - 1)(i) - prices(i - 1)is often negative. A start of 0 would act as a free purchase, and the method would report profits that no sequence of trades can reach. - Nested
math.max. The function takes two arguments, so three values need two calls. - Buying on the day the earlier trade ends. The running value uses
dp(t - 1)(i), which includes dayiitself. That lets one trade end and the next start on the same day. Selling and rebuying at the same price gains nothing, so the answers are the same as the slower solution's.
Complexity
Time: O(k * n). Two loops, with constant work per cell. The slower solution takes O(k * n^2).
Space: O(k * n). The table has (k + 1) * (n + 1) cells. Row t uses only row t - 1, so the memory could be reduced to two rows.
Edge cases and common mistakes
- Updating
maxDiffbefore computing the cell. The candidate would then be a sale on the day of the purchase, which only addsdp(t - 1)(i), a value the maximum already contains. The result would not change, but the order shows the rule that a share is sold on a later day, so keep it. - Declaring
maxDiffoutside the loop overt. The value from rowt - 1would leak into rowtand mix up the rows. - Falling prices.
[5, 4, 3, 2, 1]withk = 2returns 0, because every sale candidate loses money and the first two options in the maximum are 0. - Large
k.[1, 2, 3, 4, 5]withk = 100returns 4. The extra transactions are unused, but the table still has 101 rows. - Empty and single-day prices. With
prices = []or one price, no sale is possible, and the method returns 0. Withk = 0the loops never run and it also returns 0. - Index off by one. The price on day
iisprices(i - 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 = [1, 2, 3, 4, 5],k = 100gives4.prices = [5, 4, 3, 2, 1],k = 2gives0.
