longest increasing subsequenceOnlogn
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The longest increasing subsequence problem asks for the longest subsequence whose values increase strictly from left to right. The subsequence does not need to be contiguous, which is what makes the problem more interesting than a simple sorted run check.
A quadratic dynamic programming solution is easy to understand, but there is a faster O(n log n) method based on binary search. That faster method is the standard approach when the input is large.
Core Idea Behind the O(n log n) Method
The efficient algorithm does not keep every subsequence. Instead, it tracks the smallest possible tail value for an increasing subsequence of each length.
Suppose tails[2] = 7. That means there exists some increasing subsequence of length three ending in 7, and among all such subsequences, 7 is the smallest tail seen so far. A smaller tail is better because it leaves more room for future values to extend the subsequence.
When a new number arrives:
- if it is larger than every tail, append it
- otherwise, replace the first tail greater than or equal to it
The replacement step does not lose the optimal answer. It improves the best tail for that subsequence length.
Length-Only Implementation
Python's bisect_left makes the binary-search step straightforward.
This prints 4 because one valid longest increasing subsequence is 2, 3, 7, 18.
Reconstruct the Actual Subsequence
If you need the elements themselves rather than just the length, store predecessor pointers and the array indexes associated with each tail length.
The exact subsequence returned depends on replacement decisions, but its length is optimal.
Why the Complexity Is O(n log n)
The algorithm processes each input element once. For each element, it performs a binary search over tails, whose size is at most n. Binary search costs log n, so the full runtime is n * log n.
Space usage is linear because the helper arrays grow in proportion to the input size.
When the Quadratic DP Still Makes Sense
The O(n^2) approach is still useful for teaching, debugging, or very small arrays. It is often easier to derive and extend when you need custom constraints. But once input sizes become large, the binary-search version is the right default.
A good engineering habit is to choose the simpler DP when clarity matters and data is small, then switch to O(n log n) when performance becomes relevant.
Common Pitfalls
- Confusing subsequence with substring or subarray. The chosen elements do not need to be adjacent.
- Using
bisect_rightfor a strictly increasing problem. That changes the behavior for duplicates. - Assuming the
tailsarray is itself the answer. It tracks best tail values, not necessarily a real subsequence. - Forgetting predecessor indexes when reconstruction is required. Length-only logic is not enough to recover elements.
- Mixing non-decreasing and strictly increasing definitions. The binary-search condition depends on that choice.
Summary
- The
O(n log n)LIS algorithm keeps minimal tail values for each subsequence length. - Binary search determines where each new element belongs.
- Replacing a tail improves future extension opportunities without losing optimality.
- '
bisect_leftis correct for strictly increasing subsequences.' - Add predecessor tracking if you need the actual subsequence, not just its length.

