Longest Increasing Subsequence
Find the longest rising sequence hidden in a list: an O(n²) table where each cell ends at one element, then an O(n log n) method with binary search.
On this page
Eight books stand on a shelf, with heights 5, 2, 8, 6, 3, 6, 9, 7. You want the books that stay to rise from left to right, strictly taller each time, and you are only allowed to take books off, never to reorder them. What is the fewest you can remove?
Turn it around: find the most books you can keep. Those form the longest increasing subsequence (LIS): the longest list of values, in their original order but not necessarily side by side, where each one is bigger than the one before. Here it has length 4, for example 2, 3, 6, 9, so you remove 4 books.
Pick the state carefully
The 1D DP habit says: let best[i] be the answer for the first i books. Try it. To decide whether book i can join, you need to know the last height of the sequence you are extending, and "the best for the first i books" does not tell you that.
So pin the end down. Make each cell about sequences that end exactly at book i. Then the last height is h[i], and extending is easy to check.
- State.
len[i]is the length of the longest increasing subsequence that ends at indexi, usingh[i]as its last value. - Recurrence. The value before
h[i]is some earlier, smallerh[j]. Take the best of those:len[i] = 1 + max(len[j])over allj < iwithh[j] < h[i]. - Base case. If no earlier value is smaller,
len[i] = 1: the book on its own. - Order. Each cell reads cells to its left. Fill left to right.
- Answer. The largest value anywhere in
len, because the longest sequence can end at any book.
Unlike the shifts in the 1D lesson, each cell here looks back at every earlier cell, not just two.
for i from 0 to n - 1: len[i] = 1 # just h[i] on its own for j from 0 to i - 1: if h[j] < h[i]: len[i] = max(len[i], len[j] + 1)return max(len)- heights05i12283643566977len01i1234567
1Height 5. Nothing comes before it, so the only sequence ending here is the 5 itself:
len[0] = 1. - heights0512i283643566977len0111i234567
2Height 2. No earlier book is shorter, so nothing can come before it:
len[1] = 1. - heights051228i3643566977len011122i34567
3Height 8. Both earlier books are shorter, so 8 can extend a sequence ending at either. The best of those has length 1, so
len[2] = 1 + 1 = 2. - heights05122836i43566977len01112232i4567
4Height 6. Shorter books before it: 5, 2. The longest sequence ending at one of them has length 1, so
len[3] = 1 + 1 = 2. - heights0512283643i566977len0111223242i567
5Height 3. Shorter books before it: 2. The longest sequence ending at one of them has length 1, so
len[4] = 1 + 1 = 2. That ties with the 8 and the 6, but ends lower, which matters next. - heights051228364356i6977len011122324253i67
6Height 6. Shorter books before it: 5, 2, 3. The longest sequence ending at one of them has length 2, so
len[5] = 2 + 1 = 3. - heights05122836435669i77len01112232425364i7
7Height 9. Shorter books before it: 5, 2, 8, 6, 3, 6. The longest sequence ending at one of them has length 3, so
len[6] = 3 + 1 = 4. - heights0512283643566977ilen0111223242536474i
8Height 7. Shorter books before it: 5, 2, 6, 3, 6. The longest sequence ending at one of them has length 3, so
len[7] = 3 + 1 = 4. - heights0512283643566977len0111223242536474
- answer
- 4
- comparisons
- 28
9The answer is the largest value in
len, which is 4, here at index 6. Walking back through shorter books with length one less gives 2, 3, 6, 9. Keep those 4 books and pull the other 4. This took 28 comparisons, one for every pair of books.
function lisLength(heights) {
const n = heights.length;
const len = new Array(n).fill(1); // each value alone has length 1
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
if (heights[j] < heights[i]) {
len[i] = Math.max(len[i], len[j] + 1); // extend the run ending at j
}
}
}
return len.reduce((a, b) => Math.max(a, b), 0); // best over every end
}
lisLength([5, 2, 8, 6, 3, 6, 9, 7]); // 4def lis_length(heights):
n = len(heights)
length = [1] * n # each value alone has length 1
for i in range(n):
for j in range(i):
if heights[j] < heights[i]:
length[i] = max(length[i], length[j] + 1) # extend the run ending at j
return max(length, default=0) # best over every end
lis_length([5, 2, 8, 6, 3, 6, 9, 7]) # 4Faster: keep the lowest ending for each length
The table compares every pair, which is about 50 million comparisons for 10,000 values. A different record brings that down to about 14 steps per value, around 140,000 in all. Keep a list called tails, where tails[k] is the smallest last value of any increasing subsequence of length k + 1 seen so far.
Why the smallest? If two sequences have the same length, the one that ends lower is easier to extend: anything that fits after the higher one also fits after the lower one. So for each length, you only need to remember the lowest ending.
For each new value x, find the first tail that is >= x. If there is none, x is bigger than every tail and makes a longer sequence: append it. Otherwise replace that tail with x: a sequence of that length can now end lower. tails stays sorted the whole time, so the search is a binary search.
tails = []for x in heights: k = first index with tails[k] >= x # binary search if k == len(tails): append x else: tails[k] = xreturn len(tails)- heights05i12283643566977tails05
1Height 5.
tailsis empty, so 5 starts a sequence of length 1. - heights0512i283643566977tails02
2Height 2. The first tail that is not shorter is 5, at index 0. Replace it: a sequence of length 1 can now end at 2 instead of 5, which leaves more room to grow.
- heights051228i3643566977tails0218
3Height 8 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 2.
- heights05122836i43566977tails0216
4Height 6. The first tail that is not shorter is 8, at index 1. Replace it: a sequence of length 2 can now end at 6 instead of 8, which leaves more room to grow.
- heights0512283643i566977tails0213
5Height 3. The first tail that is not shorter is 6, at index 1. Replace it: a sequence of length 2 can now end at 3 instead of 6, which leaves more room to grow.
- heights051228364356i6977tails021326
6Height 6 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 3.
- heights05122836435669i77tails02132639
7Height 9 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 4.
- heights0512283643566977itails02132637
8Height 7. The first tail that is not shorter is 9, at index 3. Replace it: a sequence of length 4 can now end at 7 instead of 9, which leaves more room to grow.
- heights0512283643566977tails02132637
- answer
- 4
9Four tails, so the answer is 4, the same as the table. Each height cost one binary search instead of a scan of every earlier book. Only the length of
tailsmatters, as the mistakes below explain.
function lisLengthFast(heights) {
const tails = [];
for (const x of heights) {
let lo = 0;
let hi = tails.length; // find the first tail >= x
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
tails[lo] = x; // replaces a tail, or appends when lo === tails.length
}
return tails.length;
}
lisLengthFast([5, 2, 8, 6, 3, 6, 9, 7]); // 4from bisect import bisect_left
def lis_length_fast(heights):
tails = []
for x in heights:
k = bisect_left(tails, x) # first tail >= x
if k == len(tails):
tails.append(x) # longer than anything so far
else:
tails[k] = x # same length, lower ending
return len(tails)
lis_length_fast([5, 2, 8, 6, 3, 6, 9, 7]) # 4| Approach | Time | Space | Why |
|---|---|---|---|
| Table of len[i] | O(n²) | O(n) | Each of n cells looks back at up to n earlier cells. Easy to walk back and list the sequence. |
| Tails + binary search | O(n log n) | O(n) | One binary search over at most n tails per value. Gives the length directly. |
Common mistakes
Check yourself
For heights [3, 1, 4, 1, 5], what is len in the O(n²) table?
tails is [2, 5, 9] and the next value is 6. What happens?
Why does len[i] mean "the longest sequence that ends at i" rather than "the longest within the first i values"?
What to practise next
If the tails search felt hard to follow, rework Binary Search with the "first position where the condition holds" version in mind. Then try Typing Class Progress, and write the five steps in words before you write any code.