Card 07/ 09

ConceptDifficulty: Intermediate3 min

The Idea Behind the O(n log n) Method for Increasing Subsequences

Keep the smallest possible tail for each run length, and a binary search finds where each value belongs.

The O(n log n) method finds the length of the longest increasing subsequence by keeping one list, tails, where tails[k] is the smallest value that can end an increasing subsequence of length k + 1, and placing each new value with a binary search. This card explains why that list stays sorted, traces it on the same eight numbers as Longest Increasing Subsequence in O(n²), and shows why its length is the answer but its contents may not be a subsequence at all.

A small tail is worth more than a large one, because more later values can extend it. So when a new value arrives, it either extends the longest run found until then, if it is larger than every tail, or it replaces the first tail that is not smaller than it, giving that run length a smaller ending. The position to replace is the lower bound of the value in tails, the first position holding a value at least as large, which Binary Search finds in O(log n) steps.

The trace below runs 5 2 8 6 3 6 9 7. The marker shows the position each value was placed at, by appending or by replacing.

The tails list for 5 2 8 6 3 6 9 7, one binary search per value

Input5 2 8 6 3 6 9 7

  1. 5 appended

    1. 5pos

    The tails list is empty, so 5 starts it: an increasing run of length 1 exists, ending at 5.

  2. 2 replaced

    1. 2pos

    The lower bound of 2 is position 0, so 2 replaces 5. A run of length 1 that ends at 2 is easier to extend than one that ends at 5.

  3. 8 appended

    1. 2
    2. 8pos

    8 is larger than every tail, so its lower bound is the end of the list and it is appended: a run of length 2 now exists, ending at 8.

  4. 6 replaced

    1. 2
    2. 6pos

    The lower bound of 6 is position 1, so a run of length 2 can end at 6 instead of 8.

  5. 3 replaced

    1. 2
    2. 3pos

    The lower bound of 3 is position 1 again. 2 then 3 is a run of length 2 that ends lower still.

  6. 6 appended

    1. 2
    2. 3
    3. 6pos

    6 is larger than 3, the last tail, so it is appended: a run of length 3 now exists.

  7. 9 appended

    1. 2
    2. 3
    3. 6
    4. 9pos

    9 is larger than every tail and is appended: length 4.

  8. 7 replaced

    1. 2
    2. 3
    3. 6
    4. 7pos

    The lower bound of 7 is position 3, so 7 replaces 9 and the length stays 4. The list now reads 2 3 6 7, and its length, 4, is the answer.

ResultLength 4, the length of the tails list

The list stays sorted because a value replaces the first tail at least as large as itself, so it is still larger than the tail before it and no larger than the one it replaced. Sorted is what makes the binary search possible, and each of the n values costs one search, so the whole method is O(n log n).

The length of tails is the length of the longest increasing subsequence, 4, the same as the O(n²) method gave. Its contents are only a record of the best endings, and here 2 3 6 7 happens to be an increasing subsequence of the input. It need not be. On 3 4 1 the run ended with tails 1 4: the 1 replaced the 3 because a run of length 1 ending at 1 is the better start, but 1 comes after 4 in the input, so 1 4 is not a subsequence of it. The length, 2, is still right. Recovering an actual subsequence needs a parent array as in the O(n²) method, recording for each value the position of the tail before it.

The cost of the tails method on n values
Time
O(n log n), one binary search per value
every case
Space
O(n) for tails
worst case, when the whole input increases
The answer
The length of tails, not its contents