Longest Increasing Subsequence
Medium
💭 Introduction
You’re given an array of integers. Your task is to find the length of the longest strictly increasing subsequence.
A subsequence is a sequence derived from the array by deleting some or no elements without changing the order of the remaining elements.
For example, in [10, 9, 2, 5, 3, 7, 101, 18], the longest increasing subsequence is [2, 3, 7, 101] with length 4.
Important: The subsequence doesn’t need to be contiguous!
Your goal is to find the length of the longest such subsequence.
🧩 Problem Statement
LeetCode #300 — Longest Increasing Subsequence
Given an integer array nums, return the length of the longest strictly increasing subsequence.
🔍 Example 1:
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation:
The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
🔍 Example 2:
Input: nums = [0,1,0,3,2,3]
Output: 4
Explanation:
The longest increasing subsequence is [0,1,2,3], length = 4.
🔍 Example 3:
Input: nums = [7,7,7,7,7,7,7]
Output: 1
Explanation:
All elements are equal, so the longest strictly increasing subsequence has length 1.
🧠 Step 1: Understanding the Problem
Let’s clarify what we’re looking for:
Subsequence vs Subarray:
- Subarray: Contiguous elements →
[2, 5, 3]in[10, 9, 2, 5, 3, 7] - Subsequence: Non-contiguous but maintains order →
[2, 5, 7]in[10, 9, 2, 5, 3, 7]
Strictly Increasing:
- Each element must be greater than the previous (not equal)
[1, 2, 2, 3]→ LIS is[1, 2, 3], not[1, 2, 2, 3]
Key Question:
At each position, what’s the longest increasing subsequence ending at that position?
🤔 Step 2: The Brute Force Approach
We could generate all possible subsequences and check which ones are increasing:
def lengthOfLIS(nums):
def is_increasing(subseq):
for i in range(1, len(subseq)):
if subseq[i] <= subseq[i-1]:
return False
return True
max_length = 0
n = len(nums)
# Generate all 2^n subsequences
for mask in range(1 << n):
subseq = []
for i in range(n):
if mask & (1 << i):
subseq.append(nums[i])
if is_increasing(subseq):
max_length = max(max_length, len(subseq))
return max_length
Problem: This generates $2^n$ subsequences!
⏱️ Time Complexity: $O(2^n \times n)$
📦 Space Complexity: $O(n)$
🚀 Step 3: Dynamic Programming Approach (O(n²))
Define: dp[i] = length of longest increasing subsequence ending at index i
Key Insight: For each position i, we look at all previous positions j where nums[j] < nums[i], and extend the best subsequence ending at j.
Recurrence relation:
dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i]
Base case:
dp[i] = 1 // Each element is a subsequence of length 1 by itself
Answer:
max(dp[0], dp[1], ..., dp[n-1])
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
dp = [1] * n # Each element is a subsequence of length 1
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
Let’s trace through Example 1: nums = [10, 9, 2, 5, 3, 7, 101, 18]
| Index | nums[i] | dp[i] | Explanation |
|---|---|---|---|
| 0 | 10 | 1 | Base case |
| 1 | 9 | 1 | No smaller element before |
| 2 | 2 | 1 | No smaller element before |
| 3 | 5 | 2 | Extends from 2: [2, 5] |
| 4 | 3 | 2 | Extends from 2: [2, 3] |
| 5 | 7 | 3 | Extends from 5 or 3: [2, 5, 7] or [2, 3, 7] |
| 6 | 101 | 4 | Extends from 7: [2, 5, 7, 101] |
| 7 | 18 | 4 | Extends from 7: [2, 5, 7, 18] |
Final answer: max(dp) = 4 ✓
⏱️ Time Complexity: $O(n^2)$
📦 Space Complexity: $O(n)$
⚙️ Step 4: Optimized Approach with Binary Search (O(n log n))
Key Insight: We can maintain an array tails where tails[i] is the smallest tail element of all increasing subsequences of length i+1.
Why does this work?
- If we want to extend subsequences, we prefer those with smaller tail values
- We can use binary search to find where to place each new element
Algorithm:
- Maintain
tailsarray - For each number:
- If it’s larger than all tails, append it (extends longest subsequence)
- Otherwise, replace the first tail that’s >= current number (maintains smaller tails)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails = []
for num in nums:
# Binary search for the position to insert/replace
left, right = 0, len(tails)
while left < right:
mid = (left + right) // 2
if tails[mid] < num:
left = mid + 1
else:
right = mid
# If left == len(tails), append; otherwise replace
if left == len(tails):
tails.append(num)
else:
tails[left] = num
return len(tails)
Using Python’s bisect module:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
from bisect import bisect_left
tails = []
for num in nums:
pos = bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails)
Let’s trace through Example 1: nums = [10, 9, 2, 5, 3, 7, 101, 18]
| num | Action | tails | Length |
|---|---|---|---|
| 10 | Append | [10] | 1 |
| 9 | Replace 10 | [9] | 1 |
| 2 | Replace 9 | [2] | 1 |
| 5 | Append | [2, 5] | 2 |
| 3 | Replace 5 | [2, 3] | 2 |
| 7 | Append | [2, 3, 7] | 3 |
| 101 | Append | [2, 3, 7, 101] | 4 |
| 18 | Replace 101 | [2, 3, 7, 18] | 4 |
Final answer: len(tails) = 4 ✓
Important: The tails array doesn’t represent the actual LIS, just its length!
⏱️ Time Complexity: $O(n \log n)$
📦 Space Complexity: $O(n)$
🎯 Step 5: Reconstructing the Actual LIS
If you need the actual subsequence (not just its length), use the DP approach with backtracking:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> List[int]:
if not nums:
return []
n = len(nums)
dp = [1] * n
parent = [-1] * n # Track previous element in LIS
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
parent[i] = j
# Find the index with maximum length
max_length = max(dp)
max_index = dp.index(max_length)
# Reconstruct the LIS
lis = []
current = max_index
while current != -1:
lis.append(nums[current])
current = parent[current]
return lis[::-1] # Reverse to get correct order
🎯 Key Insights
-
Subsequence vs Subarray: Subsequence doesn’t need to be contiguous
-
DP State Definition:
dp[i]= longest increasing subsequence ending at indexi -
Two Solutions: O(n²) DP is intuitive; O(n log n) binary search is optimal
-
Tails Array Trick: Maintain smallest tail for each length to enable binary search
-
Strictly Increasing: Use
<not<=for comparison
📊 Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute Force | $O(2^n \times n)$ | $O(n)$ | Generate all subsequences |
| DP (O(n²)) | $O(n^2)$ | $O(n)$ | Easy to understand |
| Binary Search | $O(n \log n)$ | $O(n)$ | Optimal solution |
| With Reconstruction | $O(n^2)$ | $O(n)$ | Returns actual LIS |
🌟 Edge Cases to Consider
- Empty array:
[]→ 0 - Single element:
[1]→ 1 - All equal:
[5, 5, 5, 5]→ 1 - Strictly decreasing:
[5, 4, 3, 2, 1]→ 1 - Already sorted:
[1, 2, 3, 4, 5]→ 5 - Negative numbers:
[-10, -5, 0, 5]→ 4
💡 Common Mistakes
- Confusing subsequence with subarray: Subsequence can skip elements
- Using
<=instead of<: Problem asks for strictly increasing - Thinking tails is the actual LIS: It’s not! It just tracks lengths
- Wrong binary search bounds: Be careful with left/right pointers
- Forgetting to return max(dp): Answer is not dp[-1]
🔍 Variations of This Problem
- Longest Decreasing Subsequence — Reverse the comparison
- Number of Longest Increasing Subsequences — Count all LIS
- Russian Doll Envelopes — 2D version of LIS
- Maximum Length of Pair Chain — Similar pattern with pairs
- Longest Arithmetic Subsequence — With arithmetic progression constraint
🧮 Step 6: Understanding the Binary Search Approach
Why does replacing elements work?
Consider tails = [2, 5, 7] and we encounter 3:
- We replace
5with3→tails = [2, 3, 7] - This doesn’t change the length (still 3)
- But now we have a better foundation for future extensions!
- If we see
4next, we can extend:[2, 3, 4]vs[2, 5, 4](invalid)
Invariant maintained:
tails[i]is always the smallest possible tail for lengthi+1- This greedy choice maximizes our chances of extending the sequence
💡 Related Problems
- Maximum Subarray - Contiguous version
- House Robber - Linear DP pattern
- Longest Common Subsequence - 2D DP variant
- Edit Distance - Similar DP thinking
- Russian Doll Envelopes - 2D LIS
🏆 Why This Problem Matters
- Interview favorite: Extremely common in technical interviews
- Multiple solutions: Tests ability to optimize from O(n²) to O(n log n)
- Binary search mastery: Non-obvious application of binary search
- DP fundamentals: Classic example of optimal substructure
- Real-world applications: Version control, data compression, bioinformatics
🎓 Key Takeaways
- Start with DP: The O(n²) solution is intuitive and correct
- Optimize with binary search: The O(n log n) solution is elegant but less obvious
- Understand the state:
dp[i]represents LIS ending at positioni - Greedy + DP: The binary search approach combines greedy choice with DP thinking
- Practice both: Know when to use each approach based on constraints
📝 Interview Tips
- Clarify requirements: Strictly increasing? Can elements repeat?
- Start with O(n²): It’s easier to explain and code
- Mention optimization: Show you know the O(n log n) solution exists
- Test edge cases: Empty array, single element, all equal
- Explain the tails array: If using binary search, explain why it works