Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Dynamic programming

Longest increasing subsequence

Lesson · 12 min read · Dynamic programming · Longest increasing subsequence

The longest run of numbers, not necessarily adjacent, that keeps increasing. The state is "the best subsequence that ends here".

The problem

A subsequence keeps some elements of a list in their original order, skipping any others. In [3, 1, 4, 1, 5, 9, 2, 6] the subsequence 1, 4, 5, 9 is strictly increasing, and no increasing subsequence is longer, so the answer is 4. Trying every subsequence means 2ⁿ of them.

The state: the best that ends here

An increasing subsequence ends at some position i, and the element before it is some earlier, smaller element. That suggests the state end(i): the length of the longest increasing subsequence that ends exactly at position i. Fixing the end is what makes the recurrence possible: to extend a subsequence by nums[i] we only need to know its last element.

Recurrence

end(i)

  1. 1 (nums[i] on its own), or
  2. 1 + end(j) for any earlier j with nums[j] < nums[i]; take the largest.

answer

  1. the largest end(i) over all i: the best subsequence can end anywhere.
50120134012013
end(5) (the 9) calls end(j) for every smaller earlier element; end(1) and end(0) are reached again and again (red).

Top-down

from functools import lru_cache

def lis(nums):
    @lru_cache(maxsize=None)
    def end(i):                       # longest increasing subsequence ending at i
        best = 1
        for j in range(i):
            if nums[j] < nums[i]:
                best = max(best, 1 + end(j))
        return best
    return max(end(i) for i in range(len(nums))) if nums else 0

Bottom-up

end(i) reads only smaller indices, so fill left to right.

end(i) for [3, 1, 4, 1, 5, 9, 2, 6].

  1. Step 1 / 8
    nums
    3011421354952667
    end
    10·1·2·3·4·5·6·7

    Nothing before 3 is smaller: end(0) = 1.

  2. Step 2 / 8
    nums
    3011421354952667
    end
    1011·2·3·4·5·6·7

    Nothing before 1 is smaller: end(1) = 1.

  3. Step 3 / 8
    nums
    3011421354952667
    end
    101122·3·4·5·6·7

    4 can follow 3, 1; the longest of those ends at length 1, so end(2) = 2.

  4. Step 4 / 8
    nums
    3011421354952667
    end
    10112213·4·5·6·7

    Nothing before 1 is smaller: end(3) = 1.

  5. Step 5 / 8
    nums
    3011421354952667
    end
    1011221334·5·6·7

    5 can follow 3, 1, 4, 1; the longest of those ends at length 2, so end(4) = 3.

  6. Step 6 / 8
    nums
    3011421354952667
    end
    101122133445·6·7

    9 can follow 3, 1, 4, 1, 5; the longest of those ends at length 3, so end(5) = 4.

  7. Step 7 / 8
    nums
    3011421354952667
    end
    10112213344526·7

    2 can follow 1, 1; the longest of those ends at length 1, so end(6) = 2.

  8. Step 8 / 8
    nums
    3011421354952667
    end
    1011221334452647

    6 can follow 3, 1, 4, 1, 5, 2; the longest of those ends at length 3, so end(7) = 4. The answer is the largest entry, 4.

Implementation

The O(n²) table in six languages:

def lis_length(nums):
    end = [1] * len(nums)                 # end[i]: longest increasing run ending at i
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                end[i] = max(end[i], end[j] + 1)
    return max(end)

The same code is the reference solution to Longest increasing subsequence, where it is compiled and run against the tests in all six languages.

TimeO(n²)
SpaceO(n)

There is an O(n log n) method: keep, for each length, the smallest possible tail of an increasing subsequence of that length, and binary-search where each new number goes. It is the next problem in this module.