Start learning free

LESSON 325 OF 348 · DYNAMIC PROGRAMMING

Best Time to Buy and Sell Stock in TypeScript: O(k * n^2) DP

Practice O(n3) 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 9
    Ctrl + Enter to check

    TYPESCRIPT · DYNAMIC PROGRAMMING

    Best Time To Buy Stock in TypeScript, explained

    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 one buy followed by one sell. You may hold only one share at a time, so you must sell before you buy again. The method returns the largest profit you can make.

    In the first example, prices = [2, 4, 1] and k = 2. Buying at 2 and selling at 4 earns 2. Buying at 1 on the last day has no later day to sell, so the answer is 2. In the second example, prices = [3, 2, 6, 5, 0, 3] and k = 2. Buy at 2 and sell at 6 for 4, then buy at 0 and sell at 3 for 3, which totals 7.

    Approach: the best profit for t transactions in i days

    The brute-force way tries every set of non-overlapping buy and sell day pairs, which grows exponentially. Dynamic programming reuses the answers to smaller questions instead.

    Let dp[t][i] be the best profit using at most t transactions during the first i days. Days are counted from 1 here, so the price on day i is prices[i - 1]. For a given cell there are two kinds of choices:

    • Day i is not the day of a sale. Then the answer is no better than what we already had: dp[t][i - 1] (the same transactions, one day fewer) or dp[t - 1][i] (one transaction fewer).
    • The last transaction sells on day i. It bought on some earlier day j, which earns prices[i - 1] - prices[j - 1]. Before day j we may have used at most t - 1 transactions, worth dp[t - 1][j - 1]. Trying every j from 1 to i - 1 and keeping the best total covers every possibility.

    Row 0 (no transactions) and column 0 (no days) are always 0, which is why the table starts out filled with zeros. The answer is dp[k][n].

    The inner loop over j makes this slower than it needs to be. Another version of this lesson replaces it with a running maximum and gets O(k * n). This one keeps the loop because it is the most direct translation of the idea.

    Step by step

    1. Store the length. n = prices.length is used in several places.
    2. Build the table. It needs k + 1 rows and n + 1 columns of zeros. Array.from with a function creates each row separately.
    3. Loop over transaction counts t from 1 to k. Row 0 stays 0.
    4. Loop over days i from 1 to n. Column 0 stays 0.
    5. Start with the known best. Set dp[t][i] to the larger of dp[t][i - 1] and dp[t - 1][i]. This covers every case without a sale on day i.
    6. Try every buy day j from 1 to i - 1. The buy must come before the sell.
    7. Compute the profit of buying on day j and selling on day i.
    8. Update the cell if that profit plus dp[t - 1][j - 1] beats the current value.
    9. Return dp[k][n].

    The solution in TypeScript

    class Solution {
        maxProfit(prices: number[], k: number): number {
            const n: number = prices.length;
            const dp: number[][] = Array.from({ length: k + 1 }, () => new Array(n + 1).fill(0));
            for (let t = 1; t <= k; t++) {
                for (let i = 1; i <= n; i++) {
                    dp[t][i] = Math.max(dp[t][i - 1], dp[t - 1][i]);
                    for (let j = 1; j < i; j++) {
                        const profit: number = prices[i - 1] - prices[j - 1];
                        dp[t][i] = Math.max(dp[t][i], dp[t - 1][j - 1] + profit);
                    }
                }
            }
            return dp[k][n];
        }
    }
    

    TypeScript details worth knowing:

    • Array.from({ length: k + 1 }, () => ...). An object with a length property is enough for Array.from to produce that many elements, and the second argument builds each one. The function runs once per row, so every row is a new array. The shortcut new Array(k + 1).fill(new Array(n + 1).fill(0)) would store one row k + 1 times, so changing one cell changes the same column in every row.
    • Math.max takes any number of arguments and returns a number, so no comparison code is needed.
    • Annotations are optional. const n: number and const profit: number repeat what the compiler already infers. They disappear when the code compiles to JavaScript.
    • Negative profits are fine. profit can be negative when the price falls. Because the cell starts at the previous best and only increases through Math.max, a losing trade is never chosen.
    • Doubles, not integers. Prices are number values, so there is no integer overflow, and ordinary prices add exactly.

    Complexity

    Time: O(k * n^2). The loops over t, i and j give k times about n times up to n steps.

    Space: O(k * n). The table has (k + 1) * (n + 1) cells. Memory grows with k even though more than n / 2 transactions can never help, because each one needs two days.

    Edge cases and common mistakes

    • No transactions. With k = 0 the table has only row 0, the loops don't run, and the result is 0.
    • Empty or one-day input. With no prices, or with a single price, nothing can be sold after a buy, so the answer is 0.
    • Falling prices. [7, 6, 4, 3, 1] with k = 3 returns 0. Doing nothing is allowed, and "at most" is what makes the table start from 0.
    • More transactions than useful. With [3, 2, 6, 5, 0, 3], k = 3 and k = 10 both return 7, the same as k = 2. The extra allowance changes nothing.
    • Off-by-one with days. The table counts days from 1, while prices counts from 0. That is why the code reads prices[i - 1] and dp[t - 1][j - 1].
    • Buying and selling on the same day. Starting j at i would add a free zero-profit trade. The loop stops at i - 1.

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