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.

18 minLesson 6 of 6
On this page
  1. Pick the state carefully
  2. Faster: keep the lowest ending for each length
  3. Common mistakes
  4. Check yourself
  5. What to practise next

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.

5 2 8 6 3 6 9 7 0 1 2 3 4 5 6 7
The heights as bars. The highlighted books, 2, 3, 6 and 9, rise from left to right. No 5 books do.

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.

  1. State. len[i] is the length of the longest increasing subsequence that ends at index i, using h[i] as its last value.
  2. Recurrence. The value before h[i] is some earlier, smaller h[j]. Take the best of those: len[i] = 1 + max(len[j]) over all j < i with h[j] < h[i].
  3. Base case. If no earlier value is smaller, len[i] = 1: the book on its own.
  4. Order. Each cell reads cells to its left. Fill left to right.
  5. 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.

Fill len[i] for heights 5, 2, 8, 6, 3, 6, 9, 7Use the arrows or ← → keys
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)
  1. heights
    05i
    12
    28
    36
    43
    56
    69
    77
    len
    01i
    1
    2
    3
    4
    5
    6
    7

    1Height 5. Nothing comes before it, so the only sequence ending here is the 5 itself: len[0] = 1.

  2. heights
    05
    12i
    28
    36
    43
    56
    69
    77
    len
    01
    11i
    2
    3
    4
    5
    6
    7

    2Height 2. No earlier book is shorter, so nothing can come before it: len[1] = 1.

  3. heights
    05
    12
    28i
    36
    43
    56
    69
    77
    len
    01
    11
    22i
    3
    4
    5
    6
    7

    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.

  4. heights
    05
    12
    28
    36i
    43
    56
    69
    77
    len
    01
    11
    22
    32i
    4
    5
    6
    7

    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.

  5. heights
    05
    12
    28
    36
    43i
    56
    69
    77
    len
    01
    11
    22
    32
    42i
    5
    6
    7

    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.

  6. heights
    05
    12
    28
    36
    43
    56i
    69
    77
    len
    01
    11
    22
    32
    42
    53i
    6
    7

    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.

  7. heights
    05
    12
    28
    36
    43
    56
    69i
    77
    len
    01
    11
    22
    32
    42
    53
    64i
    7

    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.

  8. heights
    05
    12
    28
    36
    43
    56
    69
    77i
    len
    01
    11
    22
    32
    42
    53
    64
    74i

    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.

  9. heights
    05
    12
    28
    36
    43
    56
    69
    77
    len
    01
    11
    22
    32
    42
    53
    64
    74
    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.

1 / 9
O(n²): one cell per position
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]); // 4
def 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])  # 4

Faster: 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.

The tails method on the same shelfUse the arrows or ← → keys
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)
  1. heights
    05i
    12
    28
    36
    43
    56
    69
    77
    tails
    05

    1Height 5. tails is empty, so 5 starts a sequence of length 1.

  2. heights
    05
    12i
    28
    36
    43
    56
    69
    77
    tails
    02

    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.

  3. heights
    05
    12
    28i
    36
    43
    56
    69
    77
    tails
    02
    18

    3Height 8 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 2.

  4. heights
    05
    12
    28
    36i
    43
    56
    69
    77
    tails
    02
    16

    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.

  5. heights
    05
    12
    28
    36
    43i
    56
    69
    77
    tails
    02
    13

    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.

  6. heights
    05
    12
    28
    36
    43
    56i
    69
    77
    tails
    02
    13
    26

    6Height 6 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 3.

  7. heights
    05
    12
    28
    36
    43
    56
    69i
    77
    tails
    02
    13
    26
    39

    7Height 9 is taller than every tail, so it extends the longest sequence. Append it: there is now an increasing sequence of length 4.

  8. heights
    05
    12
    28
    36
    43
    56
    69
    77i
    tails
    02
    13
    26
    37

    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.

  9. heights
    05
    12
    28
    36
    43
    56
    69
    77
    tails
    02
    13
    26
    37
    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 tails matters, as the mistakes below explain.

1 / 9
O(n log n) with binary search
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]); // 4
from 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
ApproachTimeSpaceWhy
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 searchO(n log n)O(n)One binary search over at most n tails per value. Gives the length directly.

Common mistakes

Check yourself

Quick check

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.