Start learning free

LESSON 317 OF 338 · DYNAMIC PROGRAMMING

Best Time to Buy and Sell Stock in Go: O(k n) with a Running Best

Practice O(n2) Read the full explanation

Say you have an array for which the i-th element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most k transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

Example 1:

Input: [2,4,1], k = 2
Output: 2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.

Example 2:

Input: [3,2,6,5,0,3], k = 2
Output: 7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4.
             Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
STEP 1 OF 8
    Ctrl + Enter to check

    GO · DYNAMIC PROGRAMMING

    Best Time To Buy Stock in Go, explained

    Problem overview

    The function maxProfit(prices, k) takes the daily prices of a stock and a limit k on the number of transactions, a transaction being one purchase followed by one sale. It returns the largest profit possible with at most k transactions, holding at most one share at a time.

    For prices = [2, 4, 1] and k = 2 the answer is 2 (buy at 2, sell at 4). For prices = [3, 2, 6, 5, 0, 3] and k = 2 it is 7: buy at 2 and sell at 6 for 4, then buy at 0 and sell at 3 for 3.

    Approach: keep the best purchase instead of searching for it

    The table is the same as in the slower solution to this problem. Let dp[transaction][i] be the best profit with at most transaction transactions in the first i days, and the answer is dp[k][n]. The slower version fills each cell by trying every earlier buy day j in an inner loop, which makes the total time O(k * n²).

    The inner loop can go. When day i is a sale day, the candidate profit for a purchase on day j is the profit from earlier trades plus the price difference:

    dp[transaction-1][j] - prices[j-1] + prices[i-1]
    

    Only the first two terms depend on j, and the last one depends only on i. So for a fixed transaction, a single number is enough to represent every possible purchase day: the maximum of dp[transaction-1][j] - prices[j-1] over the days seen so far. This lesson calls it maxDiff. At each day i, the best sale-today option is just maxDiff + prices[i-1], found in constant time.

    One detail: using dp[transaction-1][j] (the first j days) rather than the first j - 1 days lets a purchase follow a sale on the same day. That is harmless, because a sale and a repurchase on the same day followed by a later sale earn the same as a single longer trade, which uses fewer transactions.

    For the second example with transaction = 2, maxDiff after days 1 to 6 is -3, -2, -2, -1, 4, 4. On day 6 the sale option is 4 + 3 = 7. The row dp[2] reads [0 0 0 4 4 4 7].

    Step by step

    1. Store n as the length of prices.
    2. Build the table with makeMatrix(k+1, n+1), all zeros.
    3. Loop over budgets transaction from 1 to k. Row 0 stays zero.
    4. Declare maxDiff at the start of each budget's pass, before the day loop. It starts at math.MinInt, the smallest int, because no purchase has been seen yet and any real candidate must beat it. Starting at 0 would pretend that a free purchase exists.
    5. Loop over days i from 1 to n.
    6. Fill the cell with the best of three options: no sale today (dp[transaction][i-1]), one transaction fewer (dp[transaction-1][i]), or a sale today (maxDiff + prices[i-1]). Two nested max calls compare the three.
    7. Update maxDiff afterwards with dp[transaction-1][i] - prices[i-1]. It must come after the cell is computed, so that a stock bought on day i can only be sold on a later day.
    8. Return dp[k][n].

    The solution in Go

    func maxProfit(prices []int, k int) int {
        n := len(prices)
        dp := makeMatrix(k+1, n+1)
        for transaction := 1; transaction <= k; transaction++ {
            maxDiff := math.MinInt
            for i := 1; i <= n; i++ {
                dp[transaction][i] = max(max(dp[transaction][i-1], dp[transaction-1][i]), maxDiff+prices[i-1])
                maxDiff = max(maxDiff, dp[transaction-1][i]-prices[i-1])
            }
        }
        return dp[k][n]
    }
    

    Go details worth a closer look:

    • math.MinInt comes from the math package and is the smallest value of int on the current platform, which is -9,223,372,036,854,775,808 here. The program needs import "math". The constant is available since Go 1.17.
    • Why adding to math.MinInt is safe on day 1. Integer arithmetic in Go wraps silently on overflow, so math.MinInt + x is only a problem when x is negative. Prices are not negative, so the sum moves up from the minimum and stays valid. It is also far below any real candidate, so it never wins the max.
    • Nested max. The built-in max (Go 1.21 and later) accepts more than two arguments, so max(a, b, c) would also compile. The reference nests two calls, which does the same thing.
    • := inside the outer loop. A new maxDiff is created on every budget, which is what resets it to the minimum for each row.

    Complexity

    There are two loops of about k and n iterations, and each iteration does constant work, so the time is O(k * n), down from O(k * n²). The table still needs O(k * n) space. Since each row depends only on the row above it, keeping just two rows would cut the space to O(n).

    Edge cases and common mistakes

    • Resetting maxDiff. Declaring it once before the budget loop would carry a value from one row into the next. The values come from different rows of the table, so they must not mix.
    • Updating maxDiff before the cell. With these exact formulas the result would not change, because the extra candidate, buying and selling on day i, equals dp[transaction-1][i], which the second option already covers. Keeping the update last still states the rule plainly: a stock bought on day i is sold on a later day.
    • Initializing maxDiff to 0. That treats a free purchase as possible, and on a day-1 sale it would give prices[0] as profit instead of 0.
    • No trades possible. With k = 0, an empty price list or falling prices the answer is 0.
    • A huge k. The table still has k + 1 rows, although at most n / 2 trades can help. Capping k at n / 2 avoids allocating rows that can never matter.

    Testing

    The inputs ([2, 4, 1], 2) and ([3, 2, 6, 5, 0, 3], 2) give 2 and 7. This version returns the same answers as the slower one for every input, only faster.

    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free