Skip to content

Coding patternsBlind 75

Longest Increasing Subsequence

Keep the smallest possible last number for a run of every length, and place each new number with a binary search. O(n log n) time and O(n) space, with an O(n²) table first, a step-through, code in five languages and the Big O worked out from the code.

Given an array of integers nums, return the length of its longest strictly increasing subsequence. A subsequence keeps some of the numbers in their original order, and may skip any others. Strictly increasing means each number is bigger than the one before it.

Example 1

Input: nums = [3, 1, 8, 2, 5, 9, 4]

Output: 4

1, 2, 5, 9 skips the 3, the 8 and the 4. No increasing subsequence has 5 numbers.

nums
30118223549546

Example 2

Input: nums = [7, 7, 7]

Output: 1

Equal numbers do not count as increasing, so the best is a single 7.

Example 3

Input: nums = [9, 4, 6, 2, 7]

Output: 3

4, 6, 7.

  • 1 ≤ nums.length ≤ 2500 (call it n)
  • −10⁴ ≤ nums[i] ≤ 10⁴
Must the numbers be next to each other?
No. A subsequence may skip numbers, but must keep their order.
Do equal numbers count as increasing?
No. It must be strictly increasing.
Do we return the subsequence or its length?
Only the length.
Is there a faster way than O(n²)?
Yes, O(n log n). The follow-up usually asks for it.

The longest run that ends at a number is one more than the longest run ending at some smaller number before it. Answers built from earlier answers are dynamic programming. The faster version keeps a different kind of summary, sorted so that a binary search can place each new number.

There is no brute force tab. Trying every subsequence means 2ⁿ of them, which is hopeless even for 30 numbers, and the table below is the natural first answer.

Smallest tails O(n log n)

The table looks back at every earlier number. Instead, keep a short summary called tails:

tails[k] is the smallest number that can end an increasing run of length k + 1, among the numbers seen so far.

A smaller ending is always better, because more numbers can follow it. And tails is always sorted: a run of length k + 1 contains a run of length k that ends at an even smaller number.

For each new number, find the first tail that is not smaller than number, with a binary search.

  • If there is none, number is bigger than every tail. It extends the longest run, so append it.
  • Otherwise, replace that tail with number. A run of that length can now end at number, which is no bigger.

At the end, the number of tails is the length of the longest run.

Why it is correct: each step keeps the rule “tails[k] is the smallest possible end of a run of length k + 1” true. number can follow any run whose end is smaller than number, so the longest run it can extend is the one just before the first tail that is not smaller.

nums
30118223549546
tails
0
  1. tails = []
  2. for number in nums:
  3. low = first position with tails[low] >= number # binary search
  4. if low == len(tails): tails.append(number)
  5. else: tails[low] = number
  6. return len(tails)

tails[k] holds the smallest number that can end an increasing run of length k + 1, among the numbers seen so far. A smaller ending is better, because more numbers can follow it. tails is always sorted.

1 / 9
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Smallest tails with binary search: tails[k] is the smallest last number of
// any increasing run of length k + 1 seen so far. It stays sorted. O(n log n) time, O(n) space.
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int number : nums) {
// Find the first tail that is not smaller than x.
int low = 0, high = size;
while (low < high) {
int middle = (low + high) >>> 1;
if (tails[middle] < number) low = middle + 1;
else high = middle;
}
// Past the end: x makes a longer run. Otherwise x is a smaller tail for that length.
tails[low] = number;
if (low == size) size++;
}
return size;
}
}
Time O(n log n)Space O(n)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    The outer loop runs once per number and does a fixed job after the search: 1 step. Inside, the binary search halves the range on each look, and each look is 1 step.

    1. tails = []
    2. for number in nums:
    3. low = first position with tails[low] >= number # binary search
    4. if low == len(tails): tails.append(number)
    5. else: tails[low] = number
    6. return len(tails)
  2. 2 Count the runs on Example 1

    The table has one column per number. The first number needs no look, since tails is empty. The others need 1 or 2, because tails never holds more than 4 numbers here.

    7 numbers + 0 + 1 + 1 + 2 + 1 + 2 + 2 = 16 steps.

    x3182594
    binary search looks0112122
    total
    7 numbers + 0 + 1 + 1 + 2 + 1 + 2 + 2 = 16 steps
  3. 3 Write the count for any n

    tails holds at most n numbers, and halving a range of n numbers until it is empty takes at most ⌈log₂(n + 1)⌉ looks:

    n + n × ⌈log₂(n + 1)⌉ steps

    Check it with Example 1: 7 + 7 × 3 = 28 at most. The real run used 16, because tails stayed short.

  4. 4 Keep only the part that grows fastest

    Drop the lone n and round the log. What is left is n log n. For 2500 numbers, about 30,000 steps instead of 3 million.

    So the time is O(n log n).

  5. S Space: count the extra memory the same way

    tails holds at most n numbers. So the space is O(n).

    tails (up to n)

2,000 random arrays of 1 to 10 numbers from −3 to 3, so equal neighbors, which do not count as increasing, are common. The table of runs, the smallest tails and a check that tries every subsequence gave the same answer every time.

Largest inputsResult
2,500 numbers (the most allowed), all rising, so tails grows to its full length. Smallest tails.Under 0.01 s, and about 0.02 MB of extra memory: the tails list of 2,500 numbers, O(n).
The same 2,500 rising numbers. Table of runs, about 3 million pairs.0.2 s, and about 0.09 MB of extra memory. That is over 100 times slower than the smallest tails.