Start learning free

LESSON 350 OF 415 · DYNAMIC PROGRAMMING

Best Time to Buy and Sell Stock in Swift: At Most K Transactions

Practice O(n3) Read the full explanation

Say you have an array prices 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 (i.e. you must sell the stock before you buy again).

Example 1:

Input: prices = [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: prices = [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 9
    Ctrl + Enter to check

    SWIFT · DYNAMIC PROGRAMMING

    Best Time To Buy Stock in Swift, explained

    Problem overview

    The method maxProfit(_ prices: [Int], _ k: Int) receives the price of a stock on each day and a limit k on the number of transactions. A transaction is one buy followed by one later sell. You may hold at most one share at a time, so you must sell before you buy again. The method returns the largest total profit.

    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 the answer is 7: buy at 2 and sell at 6 for 4, then buy at 0 and sell at 3 for 3. If no profitable trade exists, the best choice is to make none and the profit is 0.

    Approach: best profit by transactions and days

    The number of ways to choose buy and sell days grows very quickly, so the lesson uses a table. 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 0.

    To compute dp[t][i], consider what happens on day i:

    • No sale on day i. The best profit is whatever was possible in the first i - 1 days with t transactions, dp[t][i - 1]. Using fewer transactions is also allowed, so dp[t - 1][i] is a valid lower bound too.
    • A sale on day i. The share was bought on some earlier day j. That transaction earns prices[i - 1] - prices[j - 1], and before day j there were at most t - 1 other transactions, worth at most dp[t - 1][j - 1].

    Take the best of all of these. The answer is dp[k][n]. For the second example, the table with k = 2 looks like this, with columns for days 0 to 6:

    t=0: 0 0 0 0 0 0 0
    t=1: 0 0 0 4 4 4 4
    t=2: 0 0 0 4 4 4 7
    

    The last cell, 7, is the answer. The cost of this approach is the inner loop over j, which makes the time O(k * n^2). The follow-up lesson on this problem removes that loop.

    One cell in detail

    Take dp[2][6] for the second example. The first max gives 4, from dp[2][5]. Then the loop over j tries each buy day. Buying on day 5 (price 0) and selling on day 6 (price 3) has profit = 3. The value before day 5 with at most one transaction is dp[1][4] = 4, so the candidate is 4 + 3 = 7. Buying on day 2 instead gives profit = 1 and dp[1][1] = 0, so the candidate is only 1. The maximum is 7.

    Step by step

    1. Store prices.count in n. It is used many times.
    2. Create the table as a var: k + 1 rows of n + 1 zeros.
    3. Loop t over 1...k. Row 0 stays 0.
    4. Loop i over 1...n. Column 0 stays 0.
    5. Start from what is already known: dp[t][i] is the larger of dp[t][i - 1] and dp[t - 1][i].
    6. Try every buy day j from 1 up to, but not including, i, with 1..<i.
    7. Compute the profit of buying on day j and selling on day i. Days start at 1 but the array starts at index 0, so day d has the price prices[d - 1].
    8. Keep the larger value. If the profit plus dp[t - 1][j - 1] beats the current dp[t][i], update it. Use max.
    9. Return dp[k][n].

    The solution in Swift

    class Solution {
        func maxProfit(_ prices: [Int], _ k: Int) -> Int {
            let n = prices.count
            var dp = [[Int]](repeating: [Int](repeating: 0, count: n + 1), count: k + 1)
            for t in 1...k {
                for i in 1...n {
                    dp[t][i] = max(dp[t][i - 1], dp[t - 1][i])
                    for j in 1..<i {
                        let profit = prices[i - 1] - prices[j - 1]
                        dp[t][i] = max(dp[t][i], dp[t - 1][j - 1] + profit)
                    }
                }
            }
            return dp[k][n]
        }
    }
    

    Swift details worth knowing:

    • Empty and invalid ranges. For i = 1 the range 1..<1 is empty and the inner loop does nothing, which is correct: no earlier day exists. A closed range such as 1...k with k = 0, or 1...n with an empty price list, has a lower bound above its upper bound, so the program stops with a run-time error. The method assumes k >= 1 and at least one price.
    • Value-type rows. The nested repeating:count: builds independent rows, so dp[t][i] = ... changes only one cell.
    • max with two arguments. The global max returns the larger of its arguments. The code reads and writes the same cell dp[t][i] through it, which is safe.
    • Negative profit. profit can be negative when the price falls. The sum with dp[t - 1][j - 1] is then below the value from the first max, so a losing trade never wins.

    Complexity

    Time: O(k * n^2). There are k * n cells, and each runs a loop over up to n - 1 buy days.

    Space: O(k * n). The table has (k + 1) * (n + 1) integers.

    Edge cases and common mistakes

    • Falling prices. For [5, 4, 3] every trade loses money, and the answer is 0.
    • A single day. With [1] no sale is possible, and the answer is 0.
    • k larger than needed. With [3, 2, 6, 5, 0, 3] and k = 5, the answer is still 7: extra transactions are allowed but unnecessary, because the limit is "at most".
    • One transaction. With k = 1 the same prices give 4, buying at 2 and selling at 6.
    • Day numbers against array indices. Using prices[i] instead of prices[i - 1] reads past the end on the last day.
    • Using the wrong previous profit. Adding dp[t - 1][i - 1] or dp[t][j - 1] instead of dp[t - 1][j - 1] could let two transactions overlap or exceed the limit.

    Testing

    • prices = [2, 4, 1], k = 2 gives 2.
    • prices = [3, 2, 6, 5, 0, 3], k = 2 gives 7.
    • prices = [3, 2, 6, 5, 0, 3], k = 1 gives 4.
    • prices = [5, 4, 3], k = 2 gives 0.
    • prices = [1], k = 1 gives 0.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free