Builds on Linear DP and Lower Bound, which you haven’t finished yet.
Longest Increasing Subsequence (Patience Sorting)
You're given an integer array nums. Deleting any subset of its elements (possibly none) while keeping the rest in their original order produces a subsequence.
Find the length of the longest subsequence you can produce this way whose values keep strictly increasing from left to right.
LIS
array ➔ tails array maintenance ➔ binary search replacement| 1 | import bisect |
| 2 | |
| 3 | def length_of_lis(nums: list[int]) -> int: |
| 4 | """Longest Increasing Subsequence via Patience Sorting. |
| 5 | Time: O(N log N) | Space: O(N) |
| 6 | """ |
| 7 | # tails: sorted; tails[k] is the smallest last value of any increasing subsequence of length k + 1 seen so far |
| 8 | tails: list[int] = [] |
| 9 | |
| 10 | for x in nums: |
| 11 | # 1. Find Where x Fits: |
| 12 | # bisect_left finds the first tail >= x, the pile x can legally extend or replace. |
| 13 | idx = bisect.bisect_left(tails, x) |
| 14 | if idx == len(tails): |
| 15 | # 2. Start A Longer Subsequence: |
| 16 | # No tail is >= x, so x extends the longest subsequence found so far by one. |
| 17 | tails.append(x) |
| 18 | else: |
| 19 | # 3. Lower An Existing Tail: |
| 20 | # x is smaller than tails[idx], so it leaves more room for later values at this same length. |
| 21 | tails[idx] = x |
| 22 | |
| 23 | return len(tails) |
tails is sorted, and tails[k] is the smallest last value of any increasing subsequence of length k + 1 among the numbers seen so far.Keep tails[k] as the smallest last value of any increasing subsequence of length k + 1, and for each x replace the first tail that is >= x (found with bisect_left), or append x when there is none. A smaller tail can only help, because it leaves more room for the values that come later, and replacing never breaks the order of tails, so len(tails) is always the longest length reached so far. The trap is reading tails as the answer: it has the right length but is usually not a real subsequence, so store each element's predecessor index when you must rebuild the sequence.
See it run
Longest Increasing Subsequence with tails, step by step
tails []len(tails) 0
In the nums row each column is an index i. In the tails row each column is a position k in the list tails.
xthe number being readidxthe tail that x replacesidx (new)the new tail that x adds at the end< xa tail that is smaller than x, so it stays unchangedtaila tail that is counted in the final answer
Start: tails = [], an empty sorted list. No increasing subsequence exists yet.
Gotcha 1 of 3
- The trap
Gotcha 1: Returning
tailsas the subsequenceMistakereturn tailsFixreturn len(tails)What happens:
tails[k]is the smallest last value of any increasing subsequence of lengthk + 1. Its length is right. Its values can come from different subsequences, in the wrong order. To rebuild a real subsequence, store each element's predecessor index.Example: nums = [3, 4, 1]Mistake: returns [1, 4]Fix: returns 2
- The chain must follow the order of a second sequence rather than rising values, as in Longest Common Subsequence (LC 1143): there is no sorted order to search in. See Two-Sequence DP
Deep Dive
From the slow table to tails
The first solution is a table. dp[i] is the length of the longest increasing subsequence that ends at index i. For each i, check every earlier j with nums[j] < nums[i] and take dp[j] + 1. This is correct, and it costs O(N^2) time. For N = 2500, there are 3,123,750 pairs.
The card keeps a different state. tails[k] is the smallest last value of any increasing subsequence of length k + 1. One array of that kind is enough, because only the smallest last value matters for each length.
Why is the smallest last value enough? Two subsequences of the same length are equal for the future, except for their last value. The one with the smaller last value accepts every later number that the other accepts, and possibly more. So the larger one can be forgotten.
Why tails stays sorted, and why the length is the answer
For each x, bisect_left returns the first index idx with tails[idx] >= x. Then tails[idx - 1] < x <= tails[idx]. Writing x into tails[idx] keeps the array sorted, because x is larger than its left neighbour and not larger than the old value.
A sorted array is what allows a binary search on the next number.
If no tail is >= x, then x is larger than every tail, so x extends the longest subsequence by one, and append is correct. Otherwise x lowers the tail for length idx + 1, which can only help later numbers.
Example: for nums = [10, 9, 2, 5, 3, 7, 101, 18], tails changes as [10], [9], [2], [2, 5], [2, 3], [2, 3, 7], [2, 3, 7, 101], [2, 3, 7, 18]. The final length is 4.
The length of tails only grows when x is larger than all tails. That happens exactly when a longer increasing subsequence now exists. So len(tails) is the longest length reached so far.
tails is not a subsequence
Each entry of tails is the end of some subsequence, but the entries can come from different ones. For nums = [3, 4, 1], the final tails is [1, 4]. The values 1 and 4 are in the wrong order in nums, because 1 comes after 4. The length 2 is correct, as [3, 4] is a real subsequence of that length.
To return a real subsequence, store the index of the number in each tail, and store for each index the tail to its left at the time it was placed. Then follow these links back from the last tail.
For nums = [10, 9, 2, 5, 3, 7, 101, 18], that method returns [2, 3, 7, 18].
The cost: N searches of size log N
Each of the N numbers does one bisect_left on an array of at most N entries, which takes O(log N) steps. The append or the replacement is O(1). So the time is O(N log N).
The space is O(N) for tails in the worst case, where the input is already increasing. Rebuilding the subsequence adds O(N) for the links.
Strict and non-strict order differ by one function. bisect_left gives a strictly increasing subsequence, so equal values replace each other. bisect_right gives a non-decreasing subsequence, so equal values are appended. For [2, 2, 2], the lengths are 1 and 3.
60-Second Pitch
"The simple solution stores, for each index, the longest increasing subsequence that ends there. It compares every pair, so it takes O(N^2) time. I can do better with one observation. For each length, only the smallest last value matters, because it accepts the most later numbers. So I keep tails, where tails[k] is the smallest last value of any increasing subsequence of length k + 1. This array is always sorted. For each number I use bisect_left to find the first tail that is greater than or equal to it. I replace that tail, or I append the number when none exists. The answer is the length of tails. That is N binary searches, so O(N log N) time and O(N) space. If you need the subsequence itself, I store a link for each index."
Common Questions
- Why does the array
tailsstay sorted? bisect_leftreturns the first indexidxwheretails[idx] >= x, sotails[idx - 1] < x. The new value fits between its two neighbours. Whenxis larger than all tails, it goes at the end. In both cases the order is kept, so the next binary search is valid.- What are the time and space complexity?
O(N log N)time, because each of the N numbers needs one binary search over at most N entries. The space isO(N)fortails. The simple table solution takes O(N^2) time andO(N)space.- How do you return the subsequence and not only its length?
Store the index of each tail value, and for each index store the index of the tail to its left at the moment it was placed. After the loop, start at the last tail and follow these links back, then reverse the result. The array
tailsalone is not enough, because its values can come from different subsequences.- How do you change the solution for a non-decreasing subsequence?
Replace
bisect_leftwithbisect_right. Then an equal value is appended after the equal tails instead of replacing one. For[2, 2, 2]the result changes from 1 to 3.- How does Russian Doll Envelopes (LC 354) use this?
An envelope fits into another only when both width and height are smaller. Sort the envelopes by width ascending. For equal widths, sort by height descending, so two envelopes of the same width never form an increasing pair. Then the longest strictly increasing subsequence of the heights is the answer.
SWE Reasoning
Spot it
- You must find the longest strictly increasing chain in a sequence, and the chain keeps the original order.
- An O(N^2) table is too slow, because N can be too large for N^2 steps.
- Pairs or boxes must be nested in both dimensions, as in Russian Doll Envelopes (LC 354). Sorting by one dimension turns the problem into an increasing subsequence on the other.
- You need a length, not the number of such subsequences. For the count, as in Number of Longest Increasing Subsequence (LC 673), a plain table is used.
- Among subsequences of the same length, the one with the smaller last value is always at least as good.
The invariant, step by step
tailsstarts empty. The invariant holds, because no number has been seen.- A number
xarrives.bisect_left(tails, x)findsidx, the first tail that is>= x. - If
idx == len(tails), every tail is smaller thanx. Soxcan follow the longest subsequence. The code appendsx, and a new length now exists. - Otherwise
tails[idx] >= x. A subsequence of lengthidx + 1can end atx, andxis not larger than the old tail. The code writestails[idx] = x. - In both cases
tailsis still sorted, becausetails[idx - 1] < x <= tails[idx]. - After all numbers,
tails[k]is the smallest last value for lengthk + 1, andlen(tails)is the longest length. The code returnslen(tails).
Traps
- Return
tails. It has the right length, but its values can come from different subsequences. For[3, 4, 1], it is[1, 4]. Returnlen(tails), or store links to rebuild a real subsequence. - Use
bisect_rightfor a strict order. Equal values are then appended, and[2, 2, 2]gives 3 instead of 1. - Use
bisect_leftwhen equal values are allowed. The problem then wants a non-decreasing subsequence, andbisect_leftreturns too small a length. - Sort envelopes of equal width by ascending height. Two envelopes with the same width then look nestable. Sort by width ascending and by height descending.
- Binary search on the table. The values
dp[i]of the O(N^2) table are not sorted, sobisecton them gives wrong answers. Searchtails, which is sorted.
Practice
Builds on Linear DP and Lower Bound, which you haven’t finished yet.
Longest Increasing Subsequence (Patience Sorting)
You're given an integer array nums. Deleting any subset of its elements (possibly none) while keeping the rest in their original order produces a subsequence.
Find the length of the longest subsequence you can produce this way whose values keep strictly increasing from left to right.
LIS
array ➔ tails array maintenance ➔ binary search replacement| 1 | import bisect |
| 2 | |
| 3 | def length_of_lis(nums: list[int]) -> int: |
| 4 | """Longest Increasing Subsequence via Patience Sorting. |
| 5 | Time: O(N log N) | Space: O(N) |
| 6 | """ |
| 7 | # tails: sorted; tails[k] is the smallest last value of any increasing subsequence of length k + 1 seen so far |
| 8 | tails: list[int] = [] |
| 9 | |
| 10 | for x in nums: |
| 11 | # 1. Find Where x Fits: |
| 12 | # bisect_left finds the first tail >= x, the pile x can legally extend or replace. |
| 13 | idx = bisect.bisect_left(tails, x) |
| 14 | if idx == len(tails): |
| 15 | # 2. Start A Longer Subsequence: |
| 16 | # No tail is >= x, so x extends the longest subsequence found so far by one. |
| 17 | tails.append(x) |
| 18 | else: |
| 19 | # 3. Lower An Existing Tail: |
| 20 | # x is smaller than tails[idx], so it leaves more room for later values at this same length. |
| 21 | tails[idx] = x |
| 22 | |
| 23 | return len(tails) |
tails is sorted, and tails[k] is the smallest last value of any increasing subsequence of length k + 1 among the numbers seen so far.Keep tails[k] as the smallest last value of any increasing subsequence of length k + 1, and for each x replace the first tail that is >= x (found with bisect_left), or append x when there is none. A smaller tail can only help, because it leaves more room for the values that come later, and replacing never breaks the order of tails, so len(tails) is always the longest length reached so far. The trap is reading tails as the answer: it has the right length but is usually not a real subsequence, so store each element's predecessor index when you must rebuild the sequence.
See it run
Longest Increasing Subsequence with tails, step by step
tails []len(tails) 0
In the nums row each column is an index i. In the tails row each column is a position k in the list tails.
xthe number being readidxthe tail that x replacesidx (new)the new tail that x adds at the end< xa tail that is smaller than x, so it stays unchangedtaila tail that is counted in the final answer
Start: tails = [], an empty sorted list. No increasing subsequence exists yet.
Gotcha 1 of 3
- The trap
Gotcha 1: Returning
tailsas the subsequenceMistakereturn tailsFixreturn len(tails)What happens:
tails[k]is the smallest last value of any increasing subsequence of lengthk + 1. Its length is right. Its values can come from different subsequences, in the wrong order. To rebuild a real subsequence, store each element's predecessor index.Example: nums = [3, 4, 1]Mistake: returns [1, 4]Fix: returns 2
- The chain must follow the order of a second sequence rather than rising values, as in Longest Common Subsequence (LC 1143): there is no sorted order to search in. See Two-Sequence DP
Deep Dive
From the slow table to tails
The first solution is a table. dp[i] is the length of the longest increasing subsequence that ends at index i. For each i, check every earlier j with nums[j] < nums[i] and take dp[j] + 1. This is correct, and it costs O(N^2) time. For N = 2500, there are 3,123,750 pairs.
The card keeps a different state. tails[k] is the smallest last value of any increasing subsequence of length k + 1. One array of that kind is enough, because only the smallest last value matters for each length.
Why is the smallest last value enough? Two subsequences of the same length are equal for the future, except for their last value. The one with the smaller last value accepts every later number that the other accepts, and possibly more. So the larger one can be forgotten.
Why tails stays sorted, and why the length is the answer
For each x, bisect_left returns the first index idx with tails[idx] >= x. Then tails[idx - 1] < x <= tails[idx]. Writing x into tails[idx] keeps the array sorted, because x is larger than its left neighbour and not larger than the old value.
A sorted array is what allows a binary search on the next number.
If no tail is >= x, then x is larger than every tail, so x extends the longest subsequence by one, and append is correct. Otherwise x lowers the tail for length idx + 1, which can only help later numbers.
Example: for nums = [10, 9, 2, 5, 3, 7, 101, 18], tails changes as [10], [9], [2], [2, 5], [2, 3], [2, 3, 7], [2, 3, 7, 101], [2, 3, 7, 18]. The final length is 4.
The length of tails only grows when x is larger than all tails. That happens exactly when a longer increasing subsequence now exists. So len(tails) is the longest length reached so far.
tails is not a subsequence
Each entry of tails is the end of some subsequence, but the entries can come from different ones. For nums = [3, 4, 1], the final tails is [1, 4]. The values 1 and 4 are in the wrong order in nums, because 1 comes after 4. The length 2 is correct, as [3, 4] is a real subsequence of that length.
To return a real subsequence, store the index of the number in each tail, and store for each index the tail to its left at the time it was placed. Then follow these links back from the last tail.
For nums = [10, 9, 2, 5, 3, 7, 101, 18], that method returns [2, 3, 7, 18].
The cost: N searches of size log N
Each of the N numbers does one bisect_left on an array of at most N entries, which takes O(log N) steps. The append or the replacement is O(1). So the time is O(N log N).
The space is O(N) for tails in the worst case, where the input is already increasing. Rebuilding the subsequence adds O(N) for the links.
Strict and non-strict order differ by one function. bisect_left gives a strictly increasing subsequence, so equal values replace each other. bisect_right gives a non-decreasing subsequence, so equal values are appended. For [2, 2, 2], the lengths are 1 and 3.
60-Second Pitch
"The simple solution stores, for each index, the longest increasing subsequence that ends there. It compares every pair, so it takes O(N^2) time. I can do better with one observation. For each length, only the smallest last value matters, because it accepts the most later numbers. So I keep tails, where tails[k] is the smallest last value of any increasing subsequence of length k + 1. This array is always sorted. For each number I use bisect_left to find the first tail that is greater than or equal to it. I replace that tail, or I append the number when none exists. The answer is the length of tails. That is N binary searches, so O(N log N) time and O(N) space. If you need the subsequence itself, I store a link for each index."
Common Questions
- Why does the array
tailsstay sorted? bisect_leftreturns the first indexidxwheretails[idx] >= x, sotails[idx - 1] < x. The new value fits between its two neighbours. Whenxis larger than all tails, it goes at the end. In both cases the order is kept, so the next binary search is valid.- What are the time and space complexity?
O(N log N)time, because each of the N numbers needs one binary search over at most N entries. The space isO(N)fortails. The simple table solution takes O(N^2) time andO(N)space.- How do you return the subsequence and not only its length?
Store the index of each tail value, and for each index store the index of the tail to its left at the moment it was placed. After the loop, start at the last tail and follow these links back, then reverse the result. The array
tailsalone is not enough, because its values can come from different subsequences.- How do you change the solution for a non-decreasing subsequence?
Replace
bisect_leftwithbisect_right. Then an equal value is appended after the equal tails instead of replacing one. For[2, 2, 2]the result changes from 1 to 3.- How does Russian Doll Envelopes (LC 354) use this?
An envelope fits into another only when both width and height are smaller. Sort the envelopes by width ascending. For equal widths, sort by height descending, so two envelopes of the same width never form an increasing pair. Then the longest strictly increasing subsequence of the heights is the answer.
SWE Reasoning
Spot it
- You must find the longest strictly increasing chain in a sequence, and the chain keeps the original order.
- An O(N^2) table is too slow, because N can be too large for N^2 steps.
- Pairs or boxes must be nested in both dimensions, as in Russian Doll Envelopes (LC 354). Sorting by one dimension turns the problem into an increasing subsequence on the other.
- You need a length, not the number of such subsequences. For the count, as in Number of Longest Increasing Subsequence (LC 673), a plain table is used.
- Among subsequences of the same length, the one with the smaller last value is always at least as good.
The invariant, step by step
tailsstarts empty. The invariant holds, because no number has been seen.- A number
xarrives.bisect_left(tails, x)findsidx, the first tail that is>= x. - If
idx == len(tails), every tail is smaller thanx. Soxcan follow the longest subsequence. The code appendsx, and a new length now exists. - Otherwise
tails[idx] >= x. A subsequence of lengthidx + 1can end atx, andxis not larger than the old tail. The code writestails[idx] = x. - In both cases
tailsis still sorted, becausetails[idx - 1] < x <= tails[idx]. - After all numbers,
tails[k]is the smallest last value for lengthk + 1, andlen(tails)is the longest length. The code returnslen(tails).
Traps
- Return
tails. It has the right length, but its values can come from different subsequences. For[3, 4, 1], it is[1, 4]. Returnlen(tails), or store links to rebuild a real subsequence. - Use
bisect_rightfor a strict order. Equal values are then appended, and[2, 2, 2]gives 3 instead of 1. - Use
bisect_leftwhen equal values are allowed. The problem then wants a non-decreasing subsequence, andbisect_leftreturns too small a length. - Sort envelopes of equal width by ascending height. Two envelopes with the same width then look nestable. Sort by width ascending and by height descending.
- Binary search on the table. The values
dp[i]of the O(N^2) table are not sorted, sobisecton them gives wrong answers. Searchtails, which is sorted.