Start learning free

LESSON 458 OF 541 · DYNAMIC PROGRAMMING

Longest Increasing Subsequence in Swift: O(n²) Dynamic Programming

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

    SWIFT · DYNAMIC PROGRAMMING

    Longest Increasing Subsequence in Swift, explained

    Problem overview

    The method lengthOfLIS receives an array of integers, nums, and returns the length of its longest strictly increasing subsequence. A subsequence is what remains after deleting some elements, possibly none, without reordering the rest. It is strictly increasing when every element is larger than the one before, so two equal values can't both be in it. Only the length is returned, not the subsequence itself.

    For [10, 9, 2, 5, 3, 7, 101, 18] the answer is 4: [2, 3, 7, 101] and [2, 5, 7, 18] both qualify, and none has 5 elements. For [0, 1, 0, 3, 2, 3] the answer is also 4, through [0, 1, 2, 3]. For [7, 7, 7, 7, 7, 7, 7] it is 1, because equal values don't increase.

    The statement asks for O(n²) time and O(n) extra space. A faster O(n log n) method exists, using a binary search over the smallest possible tail for each length. This lesson builds the simpler quadratic version.

    Approach: the best subsequence ending at each index

    Checking every subsequence is hopeless, since an array of n elements has 2^n of them. Instead, ask a smaller question for each index: what is the length of the longest strictly increasing subsequence that ends with nums[i]? Call it dp[i].

    Every nums[i] alone is a subsequence of length 1, so each dp[i] starts at 1. To do better, look at every earlier index j. If nums[j] < nums[i], then nums[i] can follow the best subsequence that ends at j, giving length dp[j] + 1. Keep the largest such value over all j. The longest subsequence can end anywhere, so the final answer is the largest dp[i].

    Take the 5 at index 3 as an example. The earlier values are 10, 9 and 2. Only the 2 is smaller, and its dp entry is 1, so dp[3] becomes 2. The 10 and the 9 are larger than 5, so they are ignored. Later entries build on earlier ones, which is why the table is worth keeping.

    The table for [10, 9, 2, 5, 3, 7, 101, 18]:

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

    The 7 at index 5 ends a subsequence of length 3 (for example 2, 5, 7 or 2, 3, 7), and the 101 and the 18 each extend it to 4. For [0, 1, 0, 3, 2, 3] the table is:

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

    Step by step

    1. Store the length of nums in n. Both loops run over the indexes.
    2. Create dp as n ones, with var because the loops write into it. No entry is ever less than 1.
    3. Create best starting at 0, with var. It keeps the largest dp[i] seen so far.
    4. Loop i over 0..<n. Each dp[i] needs only entries before it, and those are final by the time the loop reaches i.
    5. Loop j over 0..<i. Only elements before nums[i] can come before it in a subsequence. For i = 0 the range is empty and the loop doesn't run.
    6. Check nums[j] < nums[i]. The test is strict, so an equal value can't extend the subsequence.
    7. Update dp[i] to max(dp[i], dp[j] + 1). This is the key line: it reuses the answer already computed for j.
    8. After the inner loop, update best with max(best, dp[i]). At that point dp[i] has seen every j < i.
    9. Return best after the outer loop.

    The solution in Swift

    class Solution {
        func lengthOfLIS(_ nums: [Int]) -> Int {
            let n = nums.count
            var dp = [Int](repeating: 1, count: n)
            var best = 0
            for i in 0..<n {
                for j in 0..<i {
                    if nums[j] < nums[i] {
                        dp[i] = max(dp[i], dp[j] + 1)
                    }
                }
                best = max(best, dp[i])
            }
            return best
        }
    }
    

    The Swift details worth a closer look:

    • Creating the table. [Int](repeating: 1, count: n) builds an array of n copies of 1. Arrays are value types, and since dp is declared with var, dp[i] = ... changes it in place.
    • Half-open ranges. 0..<i excludes i, so the inner loop never compares an element with itself, and for i = 0 it is an empty range that does nothing. Writing 0...i would include i and compare an element with itself.
    • max is a global function. max(a, b) returns the larger of two values, with no import needed.
    • Empty input. With an empty array, 0..<n is an empty range and the method returns 0. Writing 0...(n - 1) instead would crash, because 0...-1 is an invalid range in Swift.

    Complexity

    Time: O(n²). The two loops look at each pair j < i once, which is n · (n - 1) / 2 pairs, and each comparison takes constant time.

    Space: O(n). The table dp has one integer per element. Everything else is a few variables.

    Edge cases and common mistakes

    • Duplicates. [7, 7, 7, 7, 7, 7, 7] gives 1. Using <= instead of < would count equal values as increasing and return 7.
    • Sorted input. [1, 2, 3, 4, 5] gives 5, and the table is [1, 2, 3, 4, 5].
    • Decreasing input. [5, 4, 3, 2, 1] gives 1: no pair satisfies the test, so every dp[i] stays 1.
    • Returning dp[n - 1]. The longest subsequence need not end at the last element. In [1, 3, 2, 4] the table is [1, 2, 2, 3], and in [4, 10, 4, 3, 8, 9] it is [1, 2, 1, 1, 2, 3]. Taking the maximum over the table is what makes the method correct in general.
    • Starting dp at 0. A single element is a subsequence of length 1, so entries must start at 1, or a one-element array would return 0.
    • Single element. [5] gives 1.

    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.
    • [1, 2, 3, 4, 5] gives 5.
    • [5, 4, 3, 2, 1] gives 1.
    • [1, 3, 2, 4] gives 3.
    • [4, 10, 4, 3, 8, 9] gives 3.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free