Longest Increasing Subsequence – Solution & Complexity

1. Understanding the Problem

  • A subsequence keeps the original order but may skip elements.
  • We want the longest run of values that strictly increase from left to right.
  • We only need the length, not the actual subsequence.

2. Simple O(n^2) DP

  • Let dp[i] be the length of the longest increasing subsequence that ends at index i.
  • For each i, look back at every j < i; if nums[j] < nums[i], we can extend that subsequence.
  • The answer is the maximum value in dp.
def length_of_lis(nums):
    if not nums:
        return 0
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)

3. Optimizing with Patience Sorting

  • Keep a list sub where sub[k] is the smallest possible tail of an increasing subsequence of length k + 1.
  • For each number, binary search for the first tail >= it.
  • Replacing that tail keeps sub sorted and as small as possible, leaving room to grow.
import bisect

def length_of_lis(nums):
    sub = []
    for x in nums:
        i = bisect.bisect_left(sub, x)
        if i == len(sub):
            sub.append(x)   # x extends the longest subsequence
        else:
            sub[i] = x      # x replaces a larger tail
    return len(sub)

4. Why It Works

  • sub is never the real subsequence, but its length always equals the LIS length so far.
  • Using bisect_left enforces strictly increasing values (equal numbers overwrite rather than extend).
  • That is why [7,7,7,...] correctly returns 1.

5. Complexity

  • Time: O(n log n) thanks to the binary search per element.
  • Space: O(n) for the sub array in the worst case.

FAQ

See also: