Longest Increasing Subsequence (LIS)
Finds the longest strictly increasing subsequence of a sequence with O(n²) DP, with a look at the O(n log n) version.
Worth knowing first
#How does it work?
The longest increasing subsequence (LIS) is the longest strictly increasing sequence you can get by deleting some elements of a sequence of numbers. For [3, 10, 2, 1, 20, 4, 6, 7] the answer is [3, 4, 6, 7] (length 4).
dp[i] is the length of the longest increasing subsequence that ends at a[i]. For every earlier a[j] smaller than a[i], a[i] can extend the sequence ending at j: dp[i] = 1 + max(dp[j]). If there is none, dp[i] = 1.
prev[i] remembers which j was extended; following prev[] back from the largest dp value recovers the subsequence. There is also a faster method: keep the smallest ending value for each length and use binary search to get O(n log n).
#Try it yourself
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| a | 3 | 10 | 2 | 1 | 20 | 4 | 6 | 7 |
| dp | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| prev |
Type 2-10 numbers between 0 and 99. dp[i] is the length of the longest increasing subsequence ending at a[i].
#Step by step
- 01Start with dp[i] = 1 for every i.
- 02For each i, look at every earlier j: if a[j] < a[i] and dp[j] + 1 > dp[i], set dp[i] = dp[j] + 1 and prev[i] = j.
- 03The answer is the largest value in dp.
- 04Read the subsequence backwards by following prev[] from that position.
#Example code
The same algorithm in 8 languages. Every program is compiled, run and checked to print the same output.
def lis(a: list) -> list:
n = len(a)
dp = [1] * n # dp[i] = longest increasing subsequence ending at a[i]
prev = [-1] * n # the element before a[i] in that subsequence
for i in range(1, n):
for j in range(i):
if a[j] < a[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
prev[i] = j
i = dp.index(max(dp))
out = []
while i != -1:
out.append(a[i])
i = prev[i]
return out[::-1]
result = lis([3, 10, 2, 1, 20, 4, 6, 7])
print(len(result))
print(" ".join(map(str, result)))#Complexity
| Operation | Time |
|---|---|
| DP (this lesson) | O(n²) |
| With binary search (patience sorting) | O(n log n) |
| Memory | O(n) |
| Walking back for the subsequence | O(n) |
Every i looks at all earlier j, so it is O(n²). Keeping tails[k] = the smallest last element of an increasing sequence of length k + 1, each number is placed with binary search, bringing it down to O(n log n).
#When to use it?
The longest upward trend in a time series, box-nesting (Russian doll) problems, the longest consistent order in version or change logs, and the patience sorting card game all build on LIS.
#Practice
Log in to answer questions and save your progress.
Easy
What does dp[i] hold?
- 1The length of the longest increasing subsequence ending at a[i]
- 2How many elements are larger than a[i]
- 3The sum of the first i elements
- 4a[i]'s position in sorted order
What is the LIS length of [3, 10, 2, 1, 20, 4, 6, 7]?
- 14
- 23
- 35
- 48
What is the time complexity of the DP in this lesson?
- 1O(n²)
- 2O(n)
- 3O(2ⁿ)
- 4O(n³)
Medium
When a[j] < a[i], how can dp[i] be updated?
- 1dp[i] = max(dp[i], dp[j] + 1)
- 2dp[i] = dp[j]
- 3dp[i] = dp[i] + dp[j]
- 4dp[i] = a[i] − a[j]
Hard
What does the O(n log n) LIS method keep?
- 1The smallest last element of an increasing sequence for each length, updated with binary search
- 2A list of every subsequence
- 3A sorted copy and a stack
- 4How often each element appears