Longest Increasing Subsequence — O(n²) Batch Timeout
O(n²) LIS on 250k prices caused 3-hour risk system timeout.
- LIS finds the longest strictly increasing subsequence (not contiguous) in an array
- O(n²) DP: dp[i] = 1 + max(dp[j]) for all j
- O(n log n) patience sorting: maintain tails array, binary search to replace or append
- Tails array gives correct length but not the actual subsequence
- Use bisect_left for strict increase; bisect_right for non-decreasing
- Production trap: O(n²) times out for n>10⁵; O(n log n) handles millions
Imagine you're collecting stamps and you want the longest run where each new stamp has a higher number than the last — but you can skip stamps that break the chain. You don't have to take every stamp; you just want the longest streak you CAN build by picking and choosing. That's the Longest Increasing Subsequence: find the biggest set of items from a list, in order, where each one is strictly bigger than the one before it.
The Longest Increasing Subsequence (LIS) problem shows up everywhere you need to measure 'how ordered can this data get?' — from DNA sequence alignment in bioinformatics, to version-conflict resolution in distributed systems, to patience sorting algorithms used in real card games. It's deceptively simple to describe but hides serious algorithmic depth. Companies like Google, Meta, and Jane Street use it as a litmus test for whether a candidate truly understands dynamic programming versus just memorising patterns.
The core challenge: given an unsorted array of numbers, find the length (and elements) of the longest subsequence where every element is strictly greater than the previous one. The tricky part is 'subsequence' — you don't need contiguous elements. You can skip as many elements as you like, as long as the ones you pick stay in their original left-to-right order and keep going up. A brute-force check of all 2^n subsets is obviously off the table for anything real-world sized.
By the end of this article you'll understand not one but two fundamentally different approaches — the classic O(n²) DP table and the elegant O(n log n) patience-sort algorithm — know how to reconstruct the actual subsequence (not just its length), handle tricky edge cases like duplicates and negative numbers, and walk into an interview ready to explain the why behind every decision, not just recite the code.
What is Longest Increasing Subsequence (LIS)? — Plain English
The Longest Increasing Subsequence (LIS) problem: given an array of numbers, find the length of the longest subsequence where each element is strictly greater than the previous. Elements do not need to be contiguous.
Example: in [3, 1, 8, 2, 5], the LIS is [1, 2, 5] with length 3 (or [3, 8] is length 2, [1, 5] is length 2).
Real-world uses: scheduling problems where tasks must follow a precedence order, finding the maximum number of non-overlapping intervals that can be stacked, and as a building block for the patience sorting algorithm.
How Longest Increasing Subsequence (LIS) Works — Step by Step
O(n^2) DP approach: 1. Define dp[i] = length of the LIS ending at index i. 2. Initialize all dp[i] = 1 (each element is an LIS of length 1 by itself). 3. For each i from 0 to n-1: For each j from 0 to i-1: If arr[j] < arr[i]: dp[i] = max(dp[i], dp[j] + 1) 4. Return max(dp).
O(n log n) Binary Search approach (patience sorting): 1. Maintain a 'tails' array where tails[i] = the smallest tail element of all increasing subsequences of length i+1. 2. For each number x in the array: - Binary search for the leftmost position in tails where tails[pos] >= x. - If no such position: append x to tails (extends the longest subsequence). - Else: replace tails[pos] with x (allows a better (smaller tail) subsequence of that length). 3. Return len(tails).
Worked Example — Tracing the Algorithm
Array: [3, 1, 8, 2, 5, 4, 6]. Find LIS.
O(n^2) DP trace: dp = [1, 1, 1, 1, 1, 1, 1] (start) i=0 (3): no j before. dp[0]=1. i=1 (1): j=0: arr[0]=3 > 1. No update. dp[1]=1. i=2 (8): j=0: 3<8, dp[2]=max(1,dp[0]+1)=2. j=1: 1<8, dp[2]=max(2,dp[1]+1)=2. dp[2]=2. i=3 (2): j=0: 3>2. j=1: 1<2, dp[3]=max(1,dp[1]+1)=2. j=2: 8>2. dp[3]=2. i=4 (5): j=0: 3<5, dp[4]=max(1,2)=2. j=1: 1<5, dp[4]=max(2,2)=2. j=2: 8>5. j=3: 2<5, dp[4]=max(2,3)=3. dp[4]=3. i=5 (4): j=3: 2<4, dp[5]=max(1,3)=3. dp[5]=3. i=6 (6): j=0: 3<6->2. j=3: 2<6->3. j=4: 5<6->4. j=5: 4<6->4. dp[6]=4. dp = [1, 1, 2, 2, 3, 3, 4]. max=4.
O(n log n) patience sort trace: x=3: tails=[3] x=1: 1 replaces 3. tails=[1] x=8: 8 > all. tails=[1,8] x=2: 2 replaces 8. tails=[1,2] x=5: 5 > all. tails=[1,2,5] x=4: 4 replaces 5. tails=[1,2,4] x=6: 6 > all. tails=[1,2,4,6] len(tails)=4. LIS length = 4 (e.g., [1,2,5,6] or [1,2,4,6]).
Implementation
The following implementation demonstrates Longest Increasing Subsequence (LIS) with clear comments.
- Each pile is a subsequence ending with that value.
- You always place a card on the leftmost pile whose top card is >= your card.
- If no pile qualifies, start a new pile to the right.
- The number of piles at the end equals the LIS length.
- The piles themselves are not the LIS — they enable the length calculation.
Edge Cases and Duplicate Handling
Strictly increasing vs non-decreasing: the bisect variant matters. - bisect_left: replaces first element >= x → ensures strict increase (duplicates not allowed to extend). - bisect_right: replaces first element > x → allows equal values to extend the subsequence (non-decreasing).
Negative numbers: the algorithm works with any comparable elements. No special casing needed.
Empty array: return 0.
Single element: return 1.
All decreasing: LIS length = 1.
All equal: LIS length = 1 (strict) or n (non-decreasing).
Large integers: Python's int is arbitrary precision — no overflow. In Java or C++, use long or BigInteger if applicable.
Why O(n log n) LIS Is Called Patience Sorting
The name comes from the card game of patience (solitaire). Imagine you have a deck of cards in random order. You create piles by placing each card on the leftmost pile whose top card is >= the current card. If no pile qualifies, start a new pile on the right. The number of piles after processing all cards is the LIS length.
This isn't just an analogy — the algorithm was discovered by D.E. Knuth and friends in the 1960s while studying patience sorting, and it directly translates to the tails array.
Why it works: The piles maintain the invariant that the top cards are sorted in increasing order (left to right). Each new card either extends the chain (creates a new pile) or improves an existing pile by lowering its top card. The length of the longest chain is always the number of piles.
O(n²) LIS Causes 3-Hour Batch Timeout in Financial Risk System
- Always benchmark LIS with production-sized data before deploying.
- O(n²) is acceptable only for n ≤ 10,000; beyond that, switch to patience sorting.
- Include a complexity guard: if input size > threshold, enforce O(n log n).
- Set a timeout alarm for any algorithm with quadratic or higher complexity.
Key takeaways
Common mistakes to avoid
5 patternsUsing bisect_right for strict increasing LIS
Assuming tails array contains the actual LIS
Not handling empty or single-element arrays
Using O(n²) DP on large inputs without checking complexity
Confusing subsequence with subarray in recurrence
Interview Questions on This Topic
What is the recurrence relation for LIS?
Frequently Asked Questions
That's Dynamic Programming. Mark it forged?
4 min read · try the examples if you haven't