Longest Increasing Subsequence
Master this topic with zero to advance depth.
Longest Increasing Subsequence
Given an integer array nums, return the length of the longest strictly increasing subsequence.
A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements.
Visual Representation
nums = [10, 9, 2, 5, 3, 7, 101, 18]
LIS: [2, 3, 7, 18] or [2, 3, 7, 101]
Length: 4
DP Strategy:
dp[i] = length of LIS ending at index i
dp[i] = 1 + max(dp[j]) for all j < i AND nums[j] < nums[i]Examples
The longest increasing subsequence is [2, 3, 7, 101], therefore the length is 4.
Level I: Dynamic Programming (O(N^2))
Intuition
For each element nums[i], we look back at all previous elements nums[j]. If nums[i] > nums[j], then nums[i] can extend the increasing subsequence ending at j.
Thought Process
- Initialize
dparray wheredp[i] = 1(minimum length is always 1). - For each
ifrom 1 ton-1:- For each
jfrom 0 toi-1:- If
nums[i] > nums[j]:dp[i] = max(dp[i], 1 + dp[j]).
- If
- For each
- Return the maximum value in
dp.
Detailed Dry Run
nums = [10, 9, 2, 5]
| i | nums[i] | j loop | dp table | Max reached |
|---|---|---|---|---|
| 0 | 10 | - | [1, 1, 1, 1] | 1 |
| 1 | 9 | j=0 (9<10) | [1, 1, 1, 1] | 1 |
| 2 | 2 | j=0, 1 (2<9,10) | [1, 1, 1, 1] | 1 |
| 3 | 5 | j=2 (5>2) | [1, 1, 1, 2] | 2 |
Level II: Memoization (Top-Down DP)
Intuition
The brute force recursive approach recalculates the same subproblems repeatedly. We can cache results using a memo array so that each dp[i] is computed only once.
Thought Process
- Initialize
memoarray with -1. - For
solve(i), ifmemo[i] != -1, return it. - Otherwise, compute
dp[i] = 1 + max(solve(j))for allj < iwherenums[j] < nums[i]. memo[i] = dp[i].
Visual
Recursion Tree (with memoization):
solve(7): max over j < 7 where nums[j] < 18
- solve(6): 2 (cached at step 1)
- solve(5): 3 (cached at step 2)
- ... (all others are cached)Detailed Dry Run
nums = [10, 9, 2, 5]
| call | i | j checks | result |
|---|---|---|---|
| 1 | 0 | - | 1 |
| 2 | 1 | j=0 (9<10)=No | 1 |
| 3 | 2 | j=0,j=1 (2<9,10)=No | 1 |
| 4 | 3 | j=2 (5>2) | 2 (cached!) |
⚠️ Common Pitfalls & Tips
When computing max across all solve(i) calls, don't forget that lru_cache in Python caches the function but the outer max call still needs to iterate all indices.
Level III: Binary Search (O(N log N))
Intuition
This is often called Patience Sorting. We maintain a list tails where tails[i] is the smallest tail of all increasing subsequences of length i+1.
Logic Steps
- For each
xinnums:- If
xis larger than the largest tail, append it. - Otherwise, find the smallest tail \ge x using binary search and replace it with
x.
- If
- The length of
tailsis the LIS length.
Detailed Dry Run
nums = [10, 9, 2, 5]
| x | tails | Action | Reason |
|---|---|---|---|
| 10 | [10] | Append | List empty |
| 9 | [9] | Replace 10 | 9 is smaller tail for len 1 |
| 2 | [2] | Replace 9 | 2 is smaller tail for len 1 |
| 5 | [2, 5] | Append | 5 > 2, starts len 2 |
⚠️ Common Pitfalls & Tips
The Binary Search approach only gives the length of the LIS, correctly. The tails array itself may NOT represent a valid LIS (e.g., it might be mixed from different subsequences), but its size will always be correct.
Found an issue or have a suggestion?
Help us improve! Report bugs or suggest new features on our Telegram group.