Start learning free

LESSON 453 OF 536 · DYNAMIC PROGRAMMING

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

Practice O(n2) Read the full explanation

You are given a list 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

    PYTHON · DYNAMIC PROGRAMMING

    Longest Increasing Subsequence in Python, explained

    Problem overview

    The method lengthOfLIS receives a list 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 and deletes the rest. It is strictly increasing when every element is larger than the one before it, so two equal values can't both belong to it. Only the length is returned, not the subsequence.

    For nums = [10, 9, 2, 5, 3, 7, 101, 18] the answer is 4. The subsequence [2, 3, 7, 101] qualifies, and so does [2, 5, 7, 18]. No increasing subsequence has 5 elements. For [0, 1, 0, 3, 2, 3] the answer is also 4, from [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, where n is the length of nums.

    Approach: the best subsequence ending at each element

    A list of n elements has 2 to the power n subsequences, so trying them all is out of the question for a list of 2,500 elements. The lesson solves smaller versions of the problem instead.

    Let dp[i] be the length of the longest strictly increasing subsequence that ends with nums[i]. Fixing the last element is the key: it makes the question about nums[i] easy to answer from earlier answers.

    • Every dp[i] is at least 1, because nums[i] alone is such a subsequence.
    • For an earlier index j < i with nums[j] < nums[i], the element nums[i] can be appended to the best subsequence ending at j. That gives length dp[j] + 1.
    • So dp[i] is 1 plus the largest dp[j] among those j, or just 1 if no earlier element is smaller.

    The longest subsequence can end anywhere, so the answer is the largest dp[i], not necessarily the last one.

    Here are the finished tables for the examples:

    nums = [10, 9, 2, 5, 3, 7, 101, 18]
    dp   = [1, 1, 1, 2, 2, 3, 4, 4]
    
    nums = [0, 1, 0, 3, 2, 3]
    dp   = [1, 2, 1, 3, 3, 4]
    
    nums = [7, 7, 7, 7, 7, 7, 7]
    dp   = [1, 1, 1, 1, 1, 1, 1]
    

    In the first list, dp[5] is 3 because 7 can follow 3, which ends a subsequence of length 2 ([2, 3]). Then 101 extends that to 4. The 18 also reaches 4, through 7. In the second list the answer 4 sits at the last index, but in [5, 4, 3, 2, 1] every entry is 1 and the answer comes from the first.

    The statement mentions a faster O(n log n) method: keep, for each length, the smallest value an increasing subsequence of that length can end with, and place each element in that list with a binary search. This lesson practises the table, which is the idea you reuse in many other dynamic programming problems.

    Step by step

    1. Store the length of nums in n. Both loops run over the indexes.
    2. Create dp as [1] * n. Every element is a subsequence of length 1 by itself, so no entry is below 1.
    3. Start best at 0. It holds the largest length found so far.
    4. Loop over i in range(n). Each dp[i] needs only entries to its left, which are complete by the time the loop reaches i.
    5. Loop over j in range(i), the indexes before i. Only elements before nums[i] can come before it in a subsequence. For i = 0 the inner loop doesn't run.
    6. Check nums[j] < nums[i]. The test is strict, so equal values are skipped.
    7. Update dp[i] with max(dp[i], dp[j] + 1). Over all valid j, this keeps the best extension.
    8. After the inner loop, update best with max(best, dp[i]). This line sits inside the outer loop, at the same indentation as the inner for, because dp[i] is final only after every j was examined.
    9. Return best after both loops.

    The solution in Python

    class Solution:
        def lengthOfLIS(self, nums: List[int]) -> int:
            n = len(nums)
            dp = [1] * n
            best = 0
            for i in range(n):
                for j in range(i):
                    if nums[j] < nums[i]:
                        dp[i] = max(dp[i], dp[j] + 1)
                best = max(best, dp[i])
            return best
    

    Python details worth a closer look:

    • range(i) yields 0 up to i - 1, and it is empty when i is 0. That is exactly the set of earlier indexes.
    • max with two arguments returns the larger one. Here it replaces an if that compares and assigns.
    • Why best instead of max(dp). The solution tracks best as it goes, which avoids a second pass. Returning max(dp) after the loops gives the same result for a non-empty list. For an empty list max(dp) raises ValueError (max() iterable argument is empty), while this solution returns 0.
    • Indentation decides what repeats. Moving the best = max(best, dp[i]) line out of the outer loop would read only the last dp[i] and be wrong. Moving it into the inner loop would never run for i = 0, so [5] would return 0.

    Complexity

    Time: O(n²). The two loops look at each pair j < i once, which is n · (n - 1) / 2 pairs. For 2,500 elements that is about 3 million comparisons.

    Space: O(n). The table has one entry per element, and the rest are single variables.

    Edge cases and common mistakes

    • Using <= instead of <. That counts equal values as increasing. [7, 7, 7, 7, 7, 7, 7] would then return 7 instead of 1.
    • Returning dp[n - 1]. The best subsequence need not end at the last element. For [1, 2, 3, 4, 0] the last entry is 1, but the answer is 4.
    • Starting dp at 0. Then each element alone is counted as length 0, and every answer is one too small.
    • Confusing a subsequence with a subarray. A subsequence may skip elements. In [0, 1, 0, 3, 2, 3], [0, 1, 2, 3] skips the second 0 and the 3.
    • A single element. [5] returns 1: dp is [1] and the inner loop never runs.
    • Decreasing lists. [5, 4, 3, 2, 1] returns 1, since no nums[j] < nums[i] is ever true.

    Testing

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