The problem
Given an integer array nums, return the length of the longest strictly increasing subsequence. A subsequence keeps the original order but may skip elements.
Input: nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4One longest choice is [2, 3, 7, 101]; [2, 5, 7, 18] also works. No increasing subsequence has 5 elements.
How to think about it
Start with brute force. Checking every subsequence is O(2ⁿ). The classic DP improves this: dp[i] = length of the longest increasing subsequence ending at i = 1 + the max dp[j] over earlier j with nums[j] < nums[i]. That is O(n²), fine for small inputs but slow at n = 10⁵, and interviewers often ask for better.
The key insight: for each length, only the smallest tail matters. Suppose two increasing subsequences have the same length, one ending in 5 and one ending in 3. The one ending in 3 is always at least as useful, because anything that can extend 5 can also extend 3. So keep an array tails, where tails[k] is the smallest possible last value of an increasing subsequence of length k + 1.
tails stays sorted, so binary search it. For each new x, find the first position whose tail is >= x (bisect_left). If there is none, x extends the longest subsequence, so append it. Otherwise x becomes a smaller tail for that length, so overwrite it. The length of tails at the end is the answer. (The array itself is not necessarily a real subsequence; only its length is meaningful.)
Which pattern is this? 1-D DP sped up with binary search. The tell is “longest increasing subsequence” with a large n: replace “best ending at i” with “best tail for each length”.
Approach
- Start with an empty
tails. - For each
xinnums, findi = bisect_left(tails, x). - If
i == len(tails), appendx; otherwise settails[i] = x. - Return
len(tails).
Solution
import bisect
def length_of_lis(nums):
tails = [] # tails[k] = smallest tail of an LIS of length k+1
for x in nums:
i = bisect.bisect_left(tails, x) # first tail >= x
if i == len(tails): tails.append(x) # x extends the longest run
else: tails[i] = x # x is a better (smaller) tail for length i+1
return len(tails)
# length_of_lis([10,9,2,5,3,7,101,18]) -> 4 ([2,3,7,101])Dry run
On [10, 9, 2, 5, 3, 7, 101, 18]:
x |
i from bisect_left |
action | tails |
|---|---|---|---|
| 10 | 0 | append | [10] |
| 9 | 0 | replace | [9] |
| 2 | 0 | replace | [2] |
| 5 | 1 | append | [2, 5] |
| 3 | 1 | replace | [2, 3] |
| 7 | 2 | append | [2, 3, 7] |
| 101 | 3 | append | [2, 3, 7, 101] |
| 18 | 3 | replace | [2, 3, 7, 18] |
len(tails) = 4.
Complexity
- Time: O(n log n). Each of the
nelements does one binary search on an array of at mostnitems. - Space: O(n). The
tailsarray.
Edge cases and pitfalls
- Empty input returns 0; all equal (
[7, 7, 7]) returns 1. - Strict vs non-strict.
bisect_leftgives strictly increasing. Usingbisect_rightwould allow equal values (longest non-decreasing). - Reading
tailsas the answer subsequence is wrong; it can mix elements from different subsequences. Reconstruct with parent pointers if you need the actual sequence. - Know the O(n²) DP too. It’s easier to derive under pressure and extends to variants like counting the number of LIS.
Related problems
- Maximum Subarray (LeetCode 53): the contiguous cousin with the same “best ending here” state.
- Longest Common Subsequence (LeetCode 1143): subsequences again, compared across two strings with a 2-D table.
- Search Insert Position (LeetCode 35): the
bisect_leftstep on its own.
- 1.Climbing Stairs (LeetCode 70)
- 2.Min Cost Climbing Stairs (LeetCode 746)
- 3.Maximum Subarray (LeetCode 53)
- 4.House Robber (LeetCode 198)
- 5.House Robber II (LeetCode 213)
- 6.Jump Game (LeetCode 55)
- 7.Coin Change (LeetCode 322)
- 8.Coin Change II (LeetCode 518)
- 9.Word Break (LeetCode 139)
- 10.Decode Ways (LeetCode 91)
- 11.Maximum Product Subarray (LeetCode 152)
- 12.Longest Increasing Subsequence (LeetCode 300)
- 13.Partition Equal Subset Sum (LeetCode 416)