Start learning free

LESSON 457 OF 540 · DYNAMIC PROGRAMMING

Longest Increasing Subsequence in TypeScript: O(n²) DP

Practice O(n2) Read the full explanation

You are given an array of integers nums. Return the length of its longest strictly increasing subsequence.

A subsequence is what remains after deleting some elements, possibly none, without changing the order of the others. For example, [3, 6, 2, 7] is a subsequence of [0, 3, 1, 6, 2, 2, 7]. It is strictly increasing when every element is larger than the one before it, so two equal values can't both be in it.

 

Example 1:

Input: nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
Explanation: [2, 3, 7, 101] is strictly increasing, and so are [2, 5, 7, 18] and a few others of length 4.
None has 5 elements. Only the length is returned.

Example 2:

Input: nums = [0, 1, 0, 3, 2, 3]
Output: 4
Explanation: [0, 1, 2, 3] uses the first 0, the 1, the 2 and the last 3.

Example 3:

Input: nums = [7, 7, 7, 7, 7, 7, 7]
Output: 1
Explanation: Equal values don't increase, so the longest strictly increasing subsequence is a single 7.

 

Constraints:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

Note:

Your algorithm should run in O(n2) time and use O(n) extra space, where n is the length of nums.

A faster O(n log n) method exists: keep, for each length, the smallest value that an increasing subsequence of that length can end with, and place every element in that list with a binary search.



Trying every subsequence is far too slow: 2500 elements have 22500 subsequences. Solve smaller versions of the problem instead: let dp[i] be the length of the longest strictly increasing subsequence that ends with nums[i]. Every dp[i] is at least 1: the element alone is such a subsequence.

Fill dp from left to right. For every earlier index j < i with nums[j] < nums[i], the element nums[i] can be added to the end of the best subsequence that ends at j, which gives the length dp[j] + 1. So dp[i] is 1 plus the largest such dp[j], or just 1 when no earlier element is smaller.
The longest subsequence can end anywhere, so the answer is the largest dp[i], not necessarily the last one.

Every pair j < i is looked at once, and there are n · (n - 1) / 2 pairs: O(n2) time. The table takes O(n) space.
STEP 1 OF 9
    Ctrl + Enter to check

    TYPESCRIPT · DYNAMIC PROGRAMMING

    Longest Increasing Subsequence in TypeScript, explained

    Problem overview

    The method lengthOfLIS receives an array of integers, nums, and returns the length of its longest strictly increasing subsequence. A subsequence keeps some of the elements, possibly all or none, in their original order. It is strictly increasing when each element is larger than the one before, so equal values can't both be in it. Only the length is returned, not the subsequence itself.

    For nums = [10, 9, 2, 5, 3, 7, 101, 18] the answer is 4: [2, 3, 7, 101] works, and so does [2, 5, 7, 18]. No subsequence has 5 elements. For [0, 1, 0, 3, 2, 3] the answer is 4 ([0, 1, 2, 3]), and for seven 7s it is 1, because equal values don't increase.

    The statement asks for O(n²) time and O(n) extra space.

    Approach: the best subsequence that ends at each element

    Checking every subsequence is out of the question: an array of 2500 elements has 2^2500 of them. Instead, solve a smaller question for each position. Let dp[i] be the length of the longest strictly increasing subsequence that ends with nums[i].

    • Every dp[i] is at least 1, because nums[i] alone is a valid subsequence.
    • If an earlier element nums[j] (with j < i) is smaller than nums[i], then nums[i] can go after the best subsequence that ends at j. That gives a subsequence of length dp[j] + 1.
    • So dp[i] is 1 plus the largest dp[j] over all such j, or just 1 when no earlier element is smaller.

    The longest subsequence can end anywhere, so the answer is the largest entry of dp, not necessarily the last one. For [10, 9, 2, 5, 3, 7, 101, 18] the table is:

    [1, 1, 1, 2, 2, 3, 4, 4]
    

    The 3 at index 4 has dp 2 (after the 2), and the 7 at index 5 has 3 (after 2, 3). Both 101 and 18 reach 4. For [1, 3, 6, 7, 9, 4, 10, 5, 6] the table is [1, 2, 3, 4, 5, 3, 6, 4, 5]: the last entry is 5, but the answer is 6, which ends at the 10. Returning the last entry would be wrong.

    A faster O(n log n) method exists. It keeps, for each length, the smallest value an increasing subsequence of that length can end with, and places each element in that list with a binary search. This lesson uses the simpler table.

    Step by step

    1. Store the length of nums in n. Both loops run over the indexes.
    2. Create dp with n entries, all 1: each element alone is a subsequence of length 1.
    3. Declare best with let, starting at 0. It changes as the loops run, so it can't be const.
    4. Loop i over every index from left to right. When the loop reaches i, all earlier entries are final.
    5. Loop j over the earlier indexes, from 0 up to but not including i. For i = 0 there are none, and the inner loop doesn't run.
    6. Test nums[j] < nums[i]. The comparison is strict because an equal value can't follow.
    7. Update dp[i] with Math.max(dp[i], dp[j] + 1). Over all valid j, this keeps the best extension.
    8. Update best with Math.max(best, dp[i]) after the inner loop, when dp[i] is final.
    9. Return best after the outer loop.

    The solution in TypeScript

    class Solution {
        lengthOfLIS(nums: number[]): number {
            const n = nums.length;
            const dp: number[] = new Array(n).fill(1);
            let best = 0;
            for (let i = 0; i < n; i++) {
                for (let j = 0; j < i; j++) {
                    if (nums[j] < nums[i]) {
                        dp[i] = Math.max(dp[i], dp[j] + 1);
                    }
                }
                best = Math.max(best, dp[i]);
            }
            return best;
        }
    }
    

    Some TypeScript details:

    • new Array(n).fill(1) builds an array of n empty slots and puts a 1 in each. The annotation number[] is checked by the compiler only; it disappears from the compiled JavaScript.
    • const doesn't freeze the array. dp is declared with const, yet dp[i] = ... works: const only stops you from assigning a different array to the name dp. best is a plain number that gets reassigned, so it needs let.
    • Math.max takes any number of arguments and returns the largest, which is a compact way to write "keep the better value".
    • Numbers are doubles. The values here stay small, so comparisons are exact. Comparing with < is numeric because both sides are numbers; sorting is where JavaScript's default turns numbers into strings, and this solution doesn't sort.

    Complexity

    Time: O(n²). Every pair j < i is examined once, and there are n · (n - 1) / 2 pairs.

    Space: O(n). The table dp has one entry per element; everything else is a few numbers.

    Edge cases and common mistakes

    • A single element. [5] returns 1: dp[0] is 1, and best becomes 1.
    • Decreasing input. [5, 4, 3, 2, 1] returns 1. No nums[j] < nums[i] ever holds, so every entry stays 1.
    • Already increasing. [1, 2, 3, 4] returns 4, with the table [1, 2, 3, 4].
    • Equal values. Writing <= instead of < would count the seven 7s as a subsequence of length 7, but the answer is 1.
    • Returning dp[n - 1]. The longest subsequence doesn't have to end at the last element. That is why best tracks the maximum.
    • Starting dp at 0. Every element is a subsequence by itself, so the table starts at 1.
    • Mixing up subsequence and substring. The chosen elements need not be next to each other: in [0, 1, 0, 3, 2, 3] the answer skips the second 0.

    Testing

    • [10, 9, 2, 5, 3, 7, 101, 18] gives 4.
    • [0, 1, 0, 3, 2, 3] gives 4.
    • [7, 7, 7, 7, 7, 7, 7] gives 1.
    • [5] gives 1.
    • [5, 4, 3, 2, 1] gives 1.
    • [1, 2, 3, 4] gives 4.
    • [1, 3, 6, 7, 9, 4, 10, 5, 6] gives 6.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free