LeetCode 2407 - Longest Increasing Subsequence II
The problem asks us to find the length of the longest subsequence in an integer array nums such that the subsequence is strictly increasing and the difference between consecutive elements does not exceed a given integer k.
Difficulty: 🔴 Hard
Topics: Array, Divide and Conquer, Dynamic Programming, Binary Indexed Tree, Segment Tree, Queue, Monotonic Queue
Solution
Problem Understanding
The problem asks us to find the length of the longest subsequence in an integer array nums such that the subsequence is strictly increasing and the difference between consecutive elements does not exceed a given integer k. A subsequence is formed by deleting zero or more elements from the array without reordering the remaining elements.
The input nums is an array of integers with length up to 100,000, and each element can be as large as 100,000. The integer k also ranges up to 100,000. The output is a single integer representing the maximum possible length of a valid subsequence that satisfies both constraints.
The key challenges are the size of the input and the second constraint on the maximum allowed difference between consecutive elements. A naive solution that considers all subsequences will be far too slow. Important edge cases include arrays that are already strictly increasing, arrays where no two elements can be part of the same valid subsequence due to k, and arrays with repeated elements.
Approaches
The brute-force approach would attempt to generate all possible subsequences, check if each is strictly increasing and respects the k difference, and then track the maximum length. This is correct but infeasible due to the combinatorial number of subsequences: O(2^n) for an array of length n.
A better solution leverages dynamic programming along with an efficient data structure to query the maximum subsequence length within a range. Specifically, we can use a segment tree (or a binary indexed tree) to track the maximum length of a subsequence ending at any number. For each number num in nums, we query the maximum length among all numbers in the range [num - k, num - 1] and then update num’s position in the segment tree. This approach reduces the time complexity from exponential to O(n log M), where M is the maximum possible number in nums.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Enumerates all subsequences and filters based on constraints. |
| Optimal | O(n log M) | O(M) | Uses segment tree or BIT to query and update maximum subsequence length efficiently. |
Algorithm Walkthrough
- Initialize a segment tree or binary indexed tree of size equal to the maximum value in
nums. Each node will store the maximum length of a valid subsequence ending at that number. - Iterate through each element
numinnums. - For the current
num, query the segment tree to find the maximum subsequence length among numbers in the range[num - k, num - 1]. This ensures that any subsequence extended bynumrespects both strictly increasing order and the maximum allowed differencek. - The length of the subsequence ending at
numismax_length_in_range + 1. - Update the segment tree at index
numwith the new subsequence length. - Track the global maximum subsequence length as you iterate.
- Return the global maximum after processing all elements.
Why it works: At each step, the segment tree ensures that we can efficiently retrieve the longest valid subsequence that num can extend. By updating the tree, we maintain the invariant that the tree always stores the best subsequence length ending at each number. This guarantees the correct global maximum.
Python Solution
from typing import List
class SegmentTree:
def __init__(self, size: int):
self.N = 1
while self.N < size:
self.N *= 2
self.tree = [0] * (2 * self.N)
def update(self, index: int, value: int):
index += self.N
self.tree[index] = max(self.tree[index], value)
while index > 1:
index //= 2
self.tree[index] = max(self.tree[2 * index], self.tree[2 * index + 1])
def query(self, left: int, right: int) -> int:
left += self.N
right += self.N
result = 0
while left <= right:
if left % 2 == 1:
result = max(result, self.tree[left])
left += 1
if right % 2 == 0:
result = max(result, self.tree[right])
right -= 1
left //= 2
right //= 2
return result
class Solution:
def lengthOfLIS(self, nums: List[int], k: int) -> int:
max_num = max(nums)
seg_tree = SegmentTree(max_num + 1)
max_length = 0
for num in nums:
left = max(1, num - k)
right = num - 1
best_prev = seg_tree.query(left, right) if left <= right else 0
curr_length = best_prev + 1
seg_tree.update(num, curr_length)
max_length = max(max_length, curr_length)
return max_length
This Python solution uses a segment tree to track the maximum subsequence length for each number. For each num, it queries the maximum value in [num - k, num - 1] and updates the tree. The max_length is updated iteratively to maintain the global maximum.
Go Solution
package main
func lengthOfLIS(nums []int, k int) int {
maxNum := 0
for _, num := range nums {
if num > maxNum {
maxNum = num
}
}
size := 1
for size < maxNum+1 {
size *= 2
}
tree := make([]int, 2*size)
var query func(l, r int) int
query = func(l, r int) int {
res := 0
l += size
r += size
for l <= r {
if l%2 == 1 {
if tree[l] > res {
res = tree[l]
}
l++
}
if r%2 == 0 {
if tree[r] > res {
res = tree[r]
}
r--
}
l /= 2
r /= 2
}
return res
}
update := func(index, value int) {
index += size
if value > tree[index] {
tree[index] = value
}
for index > 1 {
index /= 2
if tree[2*index] > tree[2*index+1] {
tree[index] = tree[2*index]
} else {
tree[index] = tree[2*index+1]
}
}
}
maxLength := 0
for _, num := range nums {
left := num - k
if left < 1 {
left = 1
}
right := num - 1
bestPrev := 0
if left <= right {
bestPrev = query(left, right)
}
currLength := bestPrev + 1
update(num, currLength)
if currLength > maxLength {
maxLength = currLength
}
}
return maxLength
}
The Go implementation mirrors the Python version, using a segment tree. The primary differences are explicit slice initialization and integer operations, which avoid Python-specific constructs like dynamic typing.
Worked Examples
Example: nums = [4,2,1,4,3,4,5,8,15], k = 3
| num | query([num-k, num-1]) | best_prev | curr_length | update tree | max_length |
|---|---|---|---|---|---|
| 4 | query([1,3]) | 1 | 2 | 2 at index 4 | 2 |
| 2 | query([1,1]) | 1 | 2 | 2 at index 2 | 2 |
| 1 | query([-2,0]) | 0 | 1 | 1 at index 1 | 2 |
| 4 | query([1,3]) | 2 | 3 | 3 at index 4 | 3 |
| 3 | query([0,2]) | 2 | 3 | 3 at index 3 | 3 |
| 4 | query([1,3]) | 3 | 4 | 4 at index 4 | 4 |
| 5 | query([2,4]) | 4 | 5 | 5 at index 5 | 5 |
| 8 | query([5,7]) | 5 | 6 | 6 at index 8 | 6 |
| 15 | query([12,14]) | 0 | 1 | 1 at index 15 | 6 |
Final answer is 5 as expected based on constraints on differences.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log M) | Each of the n elements requires a segment tree query and update, each O(log M), where M is the max value in nums. |
| Space | O(M |