Dynamic programming · Patience sorting (tails + binary search)
nums, return the length of the longest strictly increasing subsequence. A subsequence keeps the original order but may skip elements.
Imagine a row of numbers. You may cross out as many as you like, but you may not move the rest. What is the longest run you can keep where every number is bigger than the one before it? That kept run is a subsequence.
The direct idea: for every number, choose to keep it or skip it, and see which choices give the longest run.
Each number has two choices, so n numbers give 2n possibilities. With 2,500 numbers that is hopeless.
The brute force asks the same small question over and over: "what is the longest run that ends at this number?" Let us answer it once per number. If we know the best run ending at every earlier number, how do we get it for the next one?
Look at the earlier numbers that are smaller than the new one. The new number can be added to any of their runs. So its best run is one more than the best of those. This is the first, simpler solution.
The idea. Let dp[i] be the longest run that ends exactly at index i. For each i, look at every j before it. If nums[j] < nums[i], the number at i can extend that run.
dp[i] at 1: a number alone is a run of length 1.i, check every earlier j. nums[j] < nums[i]⟶dp[i] = max(dp[i], dp[j] + 1)dp.
Cost. For each number we look at all the numbers before it, so this takes O(n²) time and O(n) space. For 2,500 numbers that is fine. It is also a fair answer to give first in an interview. But we can do better.
The idea. The table keeps far more than we need. Here is a smarter thing to keep. For each run length k, remember only the smallest number that a run of length k can end with. We call this list tails. A small tail is good, because a smaller number is easier to extend.
The list is always sorted. A run of length 3 can always be shortened to a run of length 2 that ends with a smaller number, so the tails never go down as k grows. Because the list is sorted, we can find a place in it with binary search: guess the middle, and the answer ("too small" or "not too small") tells us which half to keep.
What happens to each new number x: find the first tail that is not smaller than x.
x. x is bigger than all of them⟶add x at the end: the longest run grew by onex. found it at position k⟶replace it with x: a run of that length can now end lower, and the length does not changeBy hand, on [10, 9, 2, 5, 3, 7, 101, 18]:
[10].[9].[2].[2, 5]. The longest run is now 2.[2, 3].[2, 3, 7].[2, 3, 7, 101].[2, 3, 7, 18].There are 4 tails, so the answer is 4. Press Play to watch it. The player also has an input of four equal numbers, [7, 7, 7, 7], so you can watch where equal numbers go: they replace a tail, and they never add.
Cost. Each number does one binary search, which takes about log n steps, so the total is O(n log n).
| Approach | Time | Space |
|---|---|---|
| Try every subsequence | 2n | O(n) |
| A table of runs | O(n²) | O(n) |
| Tails with binary search | O(n log n) | O(n) |
These are the questions that usually come next. Open one to see an answer you could say out loud.
What they are checkingWhether you know that the tails list is not itself an answer. A common mistake is to print it.
A strong answerThe tails list has the right length, but it can mix numbers from different runs, so it is not always a real subsequence. For example, on [2, 5, 1] the tails become [1, 5], but the 1 comes after the 5 in the array. To get a real one, I remember for each placed number the index of the tail just before it (its "previous" pointer), and also which index sits in each tail slot. At the end I start from the last slot and follow the pointers backwards.
What they are checkingWhether you know exactly which line decides "strictly" and can change it with confidence.
A strong answerOne comparison changes. Now a tail that is equal to x can still be extended by x, so I look for the first tail that is bigger than x, not "not smaller". In code, tails[mid] < x becomes tails[mid] <= x (in Python, bisect_right instead of bisect_left). On [7, 7, 7, 7] the answer changes from 1 to 4.
What they are checkingWhether you understand why the trick is correct, or only memorised it.
A strong answerReplacing a tail with a smaller number never hurts. A run of that length that ends lower can be extended by every number that the old run could extend, and by more. The old, larger tail has no future that the new one lacks. And we do not lose the length: the length of the list only grows when a number is bigger than every tail, which is exactly when a longer run exists.