Problem
Given an array of integers, find the length of the longest subsequence that is strictly increasing. A subsequence is formed by deleting some (or no) elements from the array without changing the order of the remaining elements โ unlike a subarray, the chosen elements do not need to be adjacent.
- Input:
nums = [10, 9, 2, 5, 3, 7, 101, 18] - Output:
4 - Explanation: The subsequence
[2, 3, 7, 101](or[2, 3, 7, 18]) has length 4 and is strictly increasing.
Intuition
The core question is: for each position in the array, what is the longest increasing subsequence that ends here? Once we know that for every position, the answer is just the maximum across all positions. This bottom-up structure lets us reuse results โ the LIS ending at index i is at most one longer than the best LIS ending at any earlier index whose value is smaller than nums[i]. Going further, we can observe that for each possible LIS length, maintaining the smallest possible last element maximizes our ability to extend that subsequence โ and binary search lets us find the right slot in O(log n) instead of O(n).
Approach 1 โ DP (O(nยฒ))
For each index i, scan all previous indices to find the longest IS that can be extended by nums[i].
- Create a
dparray of lengthnwheredp[i]= length of the LIS ending at indexi. Initialize every entry to 1 (each element alone is a valid IS of length 1). - For each index
ifrom 1 ton-1, scan every earlier indexjfrom 0 toi-1. - If
nums[j] < nums[i], updatedp[i] = max(dp[i], dp[j] + 1)โ we can extend the IS ending atjby appendingnums[i]. - After filling
dp, returnmax(dp)โ the longest IS may end at any index, not necessarily the last one.
1def lengthOfLIS(nums: list[int]) -> int:
2 dp = [1] * len(nums) # LIS ending at each position starts at length 1
3
4 for i in range(1, len(nums)):
5 for j in range(i):
6 if nums[j] < nums[i]: # nums[i] can extend any IS that ends at nums[j]
7 dp[i] = max(dp[i], dp[j] + 1)
8
9 return max(dp)Time: O(nยฒ) โ for each of the n elements, we scan all preceding elements.
Space: O(n) โ the dp array stores one value per index.
Approach 2 โ Binary Search (O(n log n))
Instead of tracking the LIS length at every index, maintain a tails list where tails[k] holds the smallest possible tail element of any increasing subsequence of length k+1. Because this list is always sorted, we can use binary search.
- Initialize
tails = []. - For each
numinnums, binary searchtailsfor the first position wheretails[pos] >= num(lower bound /bisect_left). - If
pos == len(tails), appendnumโ it is larger than every tail, so it extends the longest IS found so far. - Otherwise, replace
tails[pos] = numโ a smaller tail at this IS length opens more opportunities for future extensions. - Return
len(tails)โ each index intailsaccounts for exactly one unit of IS length.
Trace on [10, 9, 2, 5, 3, 7, 101, 18]:
- 10 โ tails =
[10] - 9 โ replace pos 0 โ tails =
[9] - 2 โ replace pos 0 โ tails =
[2] - 5 โ append โ tails =
[2, 5] - 3 โ replace pos 1 โ tails =
[2, 3] - 7 โ append โ tails =
[2, 3, 7] - 101 โ append โ tails =
[2, 3, 7, 101] - 18 โ replace pos 3 โ tails =
[2, 3, 7, 18]
โ return4โ
Note: tails = [2, 3, 7, 18] does not represent an actual LIS โ the array [2, 3, 7, 101] or [2, 3, 7, 18] is the real answer. tails is purely a bookkeeping structure for counting length.
1import bisect
2
3def lengthOfLIS(nums: list[int]) -> int:
4 tails = [] # tails[k] = smallest tail of any IS of length k+1
5
6 for num in nums:
7 # find the leftmost position where tails[pos] >= num
8 pos = bisect.bisect_left(tails, num)
9 if pos == len(tails):
10 tails.append(num) # num extends the longest IS found so far
11 else:
12 tails[pos] = num # replace to keep this tail as small as possible
13
14 return len(tails)Time: O(n log n) โ one binary search per element over a list that grows at most to length n.
Space: O(n) โ the tails list holds at most n elements.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| DP | O(nยฒ) | O(n) | When n is small (โค 2500) or you need to reconstruct the actual subsequence |
| Binary Search | O(n log n) | O(n) | When n is large (up to 10โต) and only the length is needed |
Common Mistakes
- Confusing "subsequence" with "subarray" โ elements can be non-adjacent;
[2, 3, 7]is a valid subsequence of[10, 9, 2, 5, 3, 7]even though they are not next to each other. - Initializing
dp[i] = 0instead of1โ every single element is already a valid IS of length 1, so the baseline is 1, not 0. - Returning
dp[n-1]instead ofmax(dp)โ the longest IS doesn't have to end at the last element;[10, 9, 2, 5, 3, 7, 1]has its LIS ending at index 5, not 6. - Using
bisect_rightinstead ofbisect_leftโbisect_right(upper bound) would allow a value equal to an existing tail, incorrectly treating a non-strictly-increasing step as valid. - Assuming
tailsrepresents the actual LIS โ after processing[10, 9, 2, 5, 3, 7, 101, 18],tails = [2, 3, 7, 18], but the elements were never adjacent in the original array in that order;tailsis a length-counting tool, not a sequence.
Related Problems
longest-common-subsequenceโ same "longest subsequence" DP framing, generalized to two sequenceslongest-palindromic-subsequenceโ LIS applied to a string and its reverse; shares the 2D DP structureedit-distanceโ DP over sequences where each element's choices drive transitions in a similar waypartition-equal-subset-sumโ same element-by-element DP construction, different feasibility conditionunique-pathsโ foundational DP that builds intuition for overlapping subproblems before tackling LIS