Longest Increasing Subsequence (LIS) Problem
Problem Description
The Longest Increasing Subsequence (LIS) problem requires finding the longest subsequence in a given sequence of integers such that all elements in the subsequence are strictly increasing (usually non-decreasing, but strict increasing is more common). The subsequence does not need to be contiguous, but must maintain the relative order from the original sequence. For example, for the sequence [10, 9, 2, 5, 3, 7, 101, 18], one of its longest increasing subsequences is [2, 5, 7, 101], with a length of 4.
Solution Approach
There are two classic methods to solve the LIS problem: dynamic programming (O(n²)) and greedy + binary search (O(n log n)). Below, the more efficient greedy + binary search method is explained in detail.
Step 1: Understanding the Greedy Strategy
The core of the greedy strategy is to maintain an array tail, where tail[i] represents the minimum last element among all increasing subsequences of length i+1. By continuously updating tail, we ensure it remains monotonically increasing, allowing us to use binary search to optimize the insertion process.
Step 2: Algorithm Flow
- Initialization: Create an empty array
tailto store the minimum last values for increasing subsequences of different lengths. - Traverse the original sequence: For each number
num:- If
numis greater than the last element oftail(i.e., it can extend the longest subsequence), append it directly to the end oftail. - Otherwise, find the first position in
tailwhere the value is greater than or equal tonum, and replace the value at that position withnum(the goal is to make future extensions more flexible).
- If
- Result: The final length of
tailis the length of the LIS.
Step 3: Concrete Example
Take the sequence [3, 5, 2, 8, 10, 7, 9] as an example:
- Initial
tail = [] num=3:tailis empty, add directly →tail = [3]num=5: greater than the last element 3 intail, append →tail = [3, 5]num=2: less than 5, binary search finds the first position ≥2 (index 0), replace →tail = [2, 5]num=8: greater than 5, append →tail = [2, 5, 8]num=10: greater than 8, append →tail = [2, 5, 8, 10]num=7: less than 10, binary search replaces 8 →tail = [2, 5, 7, 10]num=9: less than 10, binary search replaces 10 →tail = [2, 5, 7, 9]
Final LIS length is 4 (note that tail may not be the optimal subsequence itself, but the length is correct).
Step 4: Implementation of Binary Search
Find the first position in tail that is greater than or equal to num (left-bound binary search):
def bisect_left(tail, target):
left, right = 0, len(tail) - 1
while left <= right:
mid = (left + right) // 2
if tail[mid] < target:
left = mid + 1
else:
right = mid - 1
return left # Return insertion position
Step 5: Complete Code (Python)
def length_of_lis(nums):
tail = []
for num in nums:
# Binary search for insertion position
left, right = 0, len(tail)
while left < right:
mid = (left + right) // 2
if tail[mid] < num:
left = mid + 1
else:
right = mid
# If left equals the length of tail, we need to extend
if left == len(tail):
tail.append(num)
else:
tail[left] = num
return len(tail)
Step 6: Complexity Analysis
- Time Complexity: O(n log n), as each element requires at most one binary search.
- Space Complexity: O(n), as the length of the
tailarray can be at most n.
Summary
The greedy + binary search method cleverly reduces the time complexity by transforming the problem into maintaining a monotonic sequence via the minimum last value array. In actual interviews, you might also be asked to output the specific LIS sequence (which requires additional path recording), but the core logic is based on the method described above.