On this page
Description
The task: given a sequence, find the longest subsequence, not necessarily contiguous, whose values strictly increase. Variants ask for the heaviest such subsequence, or the longest chain of nested boxes. Trying all 2^n subsequences is hopeless. The DP insight is to ask a narrower question at each position: what's the best subsequence that ends exactly here? That gives an O(n²) DP, and a "tails" array brings the length down to O(n log n).
We'll go from simple to more technical:
- The pattern: dp[i] = the longest increasing subsequence ending at i.
- A second flavor: O(n log n) with the tails array and binary search.
- Where LIS shows up (version diffs, nesting, scheduling).
- What breaks it: reading the answer from the wrong place, tails mistaken for the answer, strict vs non-strict.
The approach
Java
int lengthOfLIS(int[] nums) {
int n = nums.length, best = 1;
int[] dp = new int[n]; // dp[i] = longest increasing subsequence ENDING at i
for (int i = 0; i < n; i++) {
dp[i] = 1; // nums[i] on its own
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1); // extend a chain ending at j
}
best = Math.max(best, dp[i]); // the answer can end anywhere
}
return best;
}
Python
def length_of_lis(nums: list[int]) -> int:
n, best = len(nums), 1
dp = [1] * n # dp[i] = longest increasing subsequence ENDING at i
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1) # extend a chain ending at j
best = max(best, dp[i]) # the answer can end anywhere
return best
Fixing the last element makes the recurrence easy: a chain ending at i is some chain ending at an earlier, smaller value, plus nums[i]. The answer is the maximum over every i - on [1, 2, 0], dp at the last index is 1, but the answer is 2. Changing the combine step gives relatives: dp[j] + nums[i] with base nums[i] finds the maximum-sum increasing subsequence.
A second flavor: O(n log n) with tails
For the length alone, keep tails[k] = the smallest possible last value of an increasing subsequence of length k + 1. A smaller tail is never worse - it leaves more room to extend. Each value replaces the first tail that is >= it, or extends the list:
Java
int[] tails = new int[n];
int len = 0;
for (int x : nums) {
int lo = 0, hi = len;
while (lo < hi) { int mid = (lo + hi) >>> 1; if (tails[mid] < x) lo = mid + 1; else hi = mid; }
tails[lo] = x; // x becomes the best tail for length lo + 1
if (lo == len) len++;
}
return len;
Python
from bisect import bisect_left
def length_of_lis(nums: list[int]) -> int:
tails = []
for x in nums:
i = bisect_left(tails, x) # the first tail >= x
if i == len(tails):
tails.append(x) # x extends the longest chain
else:
tails[i] = x # x becomes the best tail for length i + 1
return len(tails)
tails stays sorted, so each step is a binary search: O(n log n) - about 1.7 million steps for n = 10^5, against 5 billion for the quadratic loop. Two cautions: tails is not an actual subsequence (for [3, 4, 1] it ends as [1, 4]), and "strictly increasing" needs the first tail >= x; for non-decreasing, search for the first tail > x ([2, 2, 2] then has length 3, not 1). In Python, bisect.bisect_left(tails, x) is exactly "the first tail >= x", and bisect_right the first tail > x. Nested envelopes reduce to this: sort by width ascending and, for equal widths, height descending, then run LIS on heights - the descending tie-break stops two envelopes of the same width from both joining a chain.
Where you'll see it
Longest-increasing-subsequence logic shows up in version-control and patience diff algorithms (matching unique lines in order), in scheduling chains of compatible tasks, in stacking and nesting problems (boxes, envelopes), and in measuring how "sorted" a sequence already is - n minus the LIS is the minimum number of elements to move to make it sorted.
Which tool for which job?
| You need | Use |
|---|---|
| LIS length, small n or custom rules | dp[i] over all j < i, O(n²) |
| LIS length, large n | tails plus binary search, O(n log n) |
| The subsequence itself | tails plus parent indices |
| The heaviest increasing subsequence | a max-Fenwick tree over values, O(n log n) |
| Nested envelopes | sort by width up and height down on ties, then LIS on heights |
| LCS of two permutations | LIS of positions, O(n log n) |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Returning dp[n - 1] | misses LIS ending earlier | the max over all dp[i] |
Reading tails as the subsequence | [3, 4, 1] gives [1, 4], which isn't a subsequence | keep parent indices |
| The wrong search for strict vs non-strict | [2, 2, 2] gives 3 or 1 wrongly | first tail ≥ x (strict), first > x (non-strict) |
Arrays.binarySearch with duplicates | returns any match, not the first | write the lower-bound search |
| Envelopes sorted with heights ascending on ties | equal widths chain together | heights descending on ties |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why is the answer max(dp[i]) and not dp[n - 1]?
Show answer
dp[i] is the best subsequence ending exactly at i. The longest one can end anywhere - for [1, 2, 0], the chain 1, 2 ends at index 1 while dp[2] is only 1.
2. After running the tails method on [3, 4, 1], tails is [1, 4]. Is that an increasing subsequence of the input?
Show answer
No - 1 comes after 4 in the input. tails[k] is the smallest tail of some length-(k + 1) subsequence; different entries can come from different subsequences. Only the length (2) is guaranteed. Keep parent indices to rebuild an actual subsequence.
3. For nested envelopes, why sort equal widths by height descending before running LIS on heights?
Show answer
Envelopes of the same width can't nest, but LIS on heights would happily chain [1, 1], [1, 2], [1, 3] if they appeared in increasing height order. Putting equal widths in decreasing height order guarantees at most one of them can join any strictly increasing chain.
Key takeaways
- dp[i] = best subsequence ending exactly at i; extend from every compatible earlier j; the answer is the max over all i.
- O(n^2) and flexible: swap the combine step for sums, counts or custom compatibility.
- For the length: tails + binary search, O(n log n); tails is not the subsequence itself.
- Strictly increasing uses the first tail >= x; non-decreasing uses the first tail > x.
- 2D chains (envelopes) reduce to LIS after sorting by one dimension, with a descending tie-break.