Skip to content
Reliable Data Engineering
Practice problem medium dynamic-programmingbinary-search
Practise with timer, notes and rubric

Longest Increasing Subsequence in O(n log n)

Pattern: Dynamic Programming · Difficulty: Medium · Asked at: Google, Microsoft, Amazon

Classic version: LeetCode 300

Problem

Return the length of the longest strictly increasing subsequence of nums (elements in order, not necessarily contiguous). Target O(n log n).

Examples

lis([10, 9, 2, 5, 3, 7, 101, 18]) → 4    # 2, 3, 7, 18
lis([0, 1, 0, 3, 2, 3])           → 4
lis([7, 7, 7, 7])                 → 1

Starter code

def lis(nums: list[int]) -> int:
    pass

Hints

Hint 1

O(n²) DP: dp[i] = longest increasing subsequence ending at i = 1 + max dp[j] for j < i with nums[j] < nums[i].

Hint 2

O(n log n): keep tails[k] = smallest possible tail of an increasing subsequence of length k+1. For each x, replace the first tail ≥ x (or append).

Where this shows up in data engineering

Shows up as “longest run of improving metrics”, ordering checks on event sequences (how many events are out of order = n − LIS), and version-history problems. It’s also the canonical example of speeding up a DP with binary search, which interviewers love as a follow-up.

Solution

from bisect import bisect_left


def lis(nums: list[int]) -> int:
    tails: list[int] = []
    for x in nums:
        i = bisect_left(tails, x)          # first tail >= x (strictly increasing)
        if i == len(tails):
            tails.append(x)
        else:
            tails[i] = x                   # a smaller tail for length i+1
    return len(tails)

Tests

Your solution should pass these:

assert lis([10, 9, 2, 5, 3, 7, 101, 18]) == 4
assert lis([0, 1, 0, 3, 2, 3]) == 4
assert lis([7, 7, 7, 7]) == 1
assert lis([]) == 0
assert lis([1, 2, 3, 4, 5]) == 5
assert lis([5, 4, 3, 2, 1]) == 1

Explanation

Invariant: tails is sorted, and tails[k] is the smallest tail value among all increasing subsequences of length k+1 seen so far. A smaller tail is always better (it’s easier to extend).

Each new x: if it’s bigger than every tail, it extends the longest subsequence; otherwise it replaces the first tail ≥ x, improving that length’s tail. tails is not an actual subsequence. Only its length is meaningful.

Complexity: O(n log n) time, O(n) space. Use bisect_right for non-decreasing (allowing equal values).

Follow-up questions

Return an actual subsequence, not just the length.

Store for each element the index of its predecessor (the tail at position i-1 when it was placed) and the index stored in each tail slot; walk back from the last tail.

Events should arrive in increasing sequence order. What's the minimum number to drop to make the stream ordered?

n - LIS(sequence_numbers) (non-decreasing variant if duplicates are allowed).