The standard DP solution for Longest Increasing Subsequence (LIS) uses two nested loops: for each element i, scan all previous elements j < i. That takes O(N^2) time, which times out when N = 100,000.
Everyone says the optimal solution is O(N log N) using Patience Sorting and Binary Search (bisect_left). But the tails array does NOT store the actual LIS sequence! How can an array that doesn’t hold the subsequence correctly tell us the exact length of the LIS?
Here is the clean C++20 Patience Sorting LIS using
std::lower_bound. On 100,000 integers, this executes in approximately 12 milliseconds.Complexity: Time is
O(N log N)and space isO(N).std::lower_boundruns binary search with branchless comparison intrinsics.This is one of the most common points of confusion when studying LIS. Let’s clear up the mystery of why the
tailsarray works even though its contents look ‘wrong’.1. What does the `tails` array actually represent?
In Patience Sorting (inspired by solitaire card games):
tails[k]stores the SMALLEST ending value of an increasing subsequence of lengthk + 1found so far.Why do we care about the smallest ending value? Because in an increasing subsequence, the smaller the number you end with, the easier it is for future numbers to be bigger than it! You want to keep your options as open as possible.
2. The Step-by-Step Card Dealing Analogy
Suppose our array is:
[10, 9, 2, 5, 3, 7, 101, 18].10:tails = [10](Best subsequence of len 1 ends with 10).9: 9 < 10. Replace 10 with 9:tails = [9](Ending with 9 is strictly better than ending with 10).2: 2 < 9. Replace 9 with 2:tails = [2].5: 5 > 2! Extend!tails = [2, 5](Best len 1 ends in 2, best len 2 ends in 5).3: 3 < 5. Replace 5 with 3:tails = [2, 3](Now best len 2 ends in 3!).7: 7 > 3! Extend!tails = [2, 3, 7](Len 3).101: Extend!tails = [2, 3, 7, 101](Len 4).18: 18 < 101. Replace 101 with 18:tails = [2, 3, 7, 18].Total length of
tailsis 4. The answer is 4!3. Why the array contents might look scrambled, but length is ALWAYS correct
Imagine if after
[2, 3, 7, 18]we saw1. We would replace2with1, resulting intails = [1, 3, 7, 18].Notice that
[1, 3, 7, 18]might not be a valid subsequence from the original array. And that doesn’t matter!Replacing
2with1only prepares the board for a hypothetical future subsequence that starts with1. It does not change the fact that a valid subsequence of length 4 ([2, 3, 7, 18]) was already locked in!The length of
tailsonly increases when a number is strictly greater than ALL existing tail values. Replacements never shrink the array length!Clean Python 3.12 Implementation with bisect_left
Complexity Breakdown
O(N log N). We iterate throughNelements, and for each element we perform binary search overtails(at most lengthN).N * log(N). For100,000elements, this finishes in 0.02 seconds (compared to ~45 seconds forO(N^2)).O(N)to hold thetailsarray.