Longest Increasing Subsequence
The longest increasing subsequence problem has an obvious O(n^2) dynamic-programming solution and a much less obvious O(n log n) one built on binary search — the gap between them is a favorite interview escalation.
Prerequisites: Longest Common Subsequence
Given a sequence of daily prices, what is the longest stretch of days (not necessarily consecutive) where the price strictly increased each time you sampled it? That's the longest increasing subsequence (LIS) problem, and it's a good test of whether you can spot a hidden binary search inside a problem that looks like plain dynamic programming.
The idea: O(n²) DP, then a smarter O(n log n)
The direct DP: let be the length of the longest increasing subsequence ending exactly at index . For each , look back at every earlier index where , and take the best chain that ends there, plus one:
with if no such exists (the element alone is a subsequence of length 1). The answer is . This is — for each , scan all earlier .
The faster method keeps an auxiliary array tails, where tails[k] is the smallest possible tail value of any increasing subsequence of length found so far. For each new number, binary-search tails for the leftmost position it can occupy (the first tail the number) and overwrite that slot, or append if the number is larger than every tail. The length of tails at the end is the LIS length. This works because keeping the tail of each length as small as possible only ever helps future extensions — never hurts — so tails is always sorted and a binary search on it is always valid, giving .
Worked example: [10, 9, 2, 5, 3, 7, 101, 18] with the fast method
Start tails = []. 10 → append, tails=[10]. 9 → binary search finds position 0 (first ), overwrite, tails=[9]. 2 → overwrite position 0, tails=[2]. 5 → larger than all, append, tails=[2,5]. 3 → binary search finds position 1 (first is 5), overwrite, tails=[2,3]. 7 → append, tails=[2,3,7]. 101 → append, tails=[2,3,7,101]. 18 → binary search finds position 3 (first is 101), overwrite, tails=[2,3,7,18]. Final length 4, matching the true LIS 2,3,7,18 (or 2,3,7,101).
import bisect
def lis_length(nums): # O(n log n)
tails = []
for x in nums:
i = bisect.bisect_left(tails, x)
if i == len(tails):
tails.append(x)
else:
tails[i] = x
return len(tails)
tails[k] is the smallest ending value achievable for an increasing subsequence of length — not a real subsequence itself, just a bound that's always safe to binary search against. Overwriting a tail with a smaller value can only make future extensions easier, never invalid.
tails is not the actual LIS and often isn't even a valid subsequence of the input — interviewers routinely trip people up by asking to reconstruct the actual sequence, which needs a separate parent-pointer array, not a read of tails itself.
Where it shows up
Interview staples: LIS itself, Russian doll envelopes (2D LIS via sort-then-1D-LIS), and "maximum length of pair chain." On a desk, LIS-style reasoning identifies the longest stretch of consistently improving performance metrics in a noisy series, and the same binary-search-on-tails pattern shows up whenever you need the length of the longest monotonic run without recomputing from scratch on every new data point.
Related concepts
Practice in interviews
Further reading
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. 15, related)