Given an integer array nums, return the length of the longest strictly increasing subsequence.
A subsequence is a sequence that can be derived from the given sequence by deleting some or no elements without changing the relative order of the remaining characters.
"cat" is a subsequence of "crabt".Example 1:
Input: nums = [9,1,4,2,3,3,7]
Output: 4Explanation: The longest increasing subsequence is [1,2,3,7], which has a length of 4.
Example 2:
Input: nums = [0,3,1,3,2,3]
Output: 4Constraints:
1 <= nums.length <= 1000-1000 <= nums[i] <= 1000
You should aim for a solution as good or better than O(n ^ 2) time and O(n ^ 2) space, where n is the size of the input array.
A subsequence is formed by selecting elements while maintaining their order. Using recursion, we can generate all subsequences. The recursive function returns the length of the longest increasing subsequence up to index i, processing from left to right. At each step, we decide whether to include or exclude the current element.
Since the sequence must be increasing, we represent choices by adding 1 when including an element and 0 when excluding it. In recursion, how can we ensure the current element is greater than the previous one? Perhaps additional information is needed to process it.
We can store the index of the previously chosen element as j, making it easier to process the current element at index i. If and only if j == -1 or nums[i] > nums[j], we include the current element and extend the recursive path. Can you determine the recurrence relation? At most, two recursive calls are made at each recursion step.
We stop the recursion when index i goes out of bounds and return 0 since no more elements can be added. The initial recursion call starts with j = -1. At each step, we include the current element if it is greater than the previous one and continue the recursion, or we exclude it and explore the next possibility. We return the maximum value obtained from both paths.
The time complexity of this approach is exponential. We can use memoization to store results of recursive calls and avoid recalculations. A hash map or a 2D array can be used to cache these results.
Before attempting this problem, you should be comfortable with:
To find the longest increasing subsequence, we consider each element and decide whether to include it. We can only include an element if it's larger than the previous one in our subsequence. This gives us two choices at each step: skip the current element or include it (if valid). We explore all possibilities recursively and return the maximum length found.
dfs(i, j) where i is the current index and j is the index of the last included element (or -1 if none).i reaches the end, return 0.nums[i] with dfs(i + 1, j).j == -1 or nums[j] < nums[i], include it with 1 + dfs(i + 1, i).The recursive solution revisits the same (i, j) pairs multiple times. We can memoize results using a 2D table indexed by current position and the last included index. Since j can range from -1 to n-1, we offset by 1 when indexing the memo table.
n x (n + 1).dfs(i, j), check memo[i][j + 1].class Solution:
def lengthOfLIS(self, nums):
n = len(nums)
memo = [[-1] * (n + 1) for _ in range(n)]
def dfs(i, j):
if i == n:
return 0
if memo[i][j + 1] != -1:
return memo[i][j + 1]
LIS = dfs(i + 1, j)
if j == -1 or nums[j] < nums[i]:
LIS = max(LIS, 1 + dfs(i + 1, i))
memo[i][j + 1] = LIS
return LIS
return dfs(0, -1)Instead of tracking both current index and last included index, we can define dfs(i) as the length of the longest increasing subsequence starting at index i. For each starting position, we look at all positions j > i where nums[j] > nums[i] and take the maximum. This reduces the state to just one dimension.
memo array of size n.dfs(i) as the LIS length starting from index i.j from i + 1 to n - 1, if nums[i] < nums[j], consider 1 + dfs(j).dfs(i) for all i.class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
memo = [-1] * n
def dfs(i):
if memo[i] != -1:
return memo[i]
LIS = 1
for j in range(i + 1, n):
if nums[i] < nums[j]:
LIS = max(LIS, 1 + dfs(j))
memo[i] = LIS
return LIS
return max(dfs(i) for i in range(n))This is the iterative version of the two-dimensional memoization approach. We fill a 2D table dp[i][j] from right to left. The value represents the longest increasing subsequence considering elements from index i onward, given that the last included element was at index j (or no element if j == -1).
dp of size (n + 1) x (n + 1).i from n - 1 down to 0.j from i - 1 down to -1:LIS = dp[i + 1][j + 1] (skipping nums[i]).j == -1 or nums[j] < nums[i], also consider 1 + dp[i + 1][i + 1].dp[i][j + 1] to the maximum.dp[0][0].class Solution:
def lengthOfLIS(self, nums):
n = len(nums)
dp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for j in range(i - 1, -2, -1):
LIS = dp[i + 1][j + 1] # Not including nums[i]
if j == -1 or nums[j] < nums[i]:
LIS = max(LIS, 1 + dp[i + 1][i + 1]) # Including nums[i]
dp[i][j + 1] = LIS
return dp[0][0]A simpler 1D approach: let LIS[i] be the length of the longest increasing subsequence starting at index i. Working from right to left, for each i, we check all j > i. If nums[i] < nums[j], we can extend the subsequence starting at j. We take the maximum extension and add 1 for the current element.
LIS of size n, initialized to 1 (each element alone is a subsequence of length 1).i from n - 1 down to 0.j from i + 1 to n - 1:nums[i] < nums[j], set LIS[i] = max(LIS[i], 1 + LIS[j]).LIS.We can use a segment tree to efficiently query the maximum LIS length among all elements smaller than the current one. First, we compress the values to indices. Then, for each element, we query the segment tree for the maximum LIS among all values less than the current value, add 1, and update the segment tree at the current value's position.
0 to n - 1 based on sorted order.num:[0, num - 1].query result + 1.num with this value.from bisect import bisect_left
class SegmentTree:
def __init__(self, N):
self.n = N
while (self.n & (self.n - 1)) != 0:
self.n += 1
self.tree = [0] * (2 * self.n)
def update(self, i, val):
self.tree[self.n + i] = val
j = (self.n + i) >> 1
while j >= 1:
self.tree[j] = max(self.tree[j << 1], self.tree[j << 1 | 1])
j >>= 1
def query(self, l, r):
if l > r:
return 0
res = float('-inf')
l += self.n
r += self.n + 1
while l < r:
if l & 1:
res = max(res, self.tree[l])
l += 1
if r & 1:
r -= 1
res = max(res, self.tree[r])
l >>= 1
r >>= 1
return res
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def compress(arr):
sortedArr = sorted(set(arr))
order = []
for num in arr:
order.append(bisect_left(sortedArr, num))
return order
nums = compress(nums)
n = len(nums)
segTree = SegmentTree(n)
LIS = 0
for num in nums:
curLIS = segTree.query(0, num - 1) + 1
segTree.update(num, curLIS)
LIS = max(LIS, curLIS)
return LISWe maintain an array dp where dp[i] is the smallest ending element of all increasing subsequences of length i + 1. This array stays sorted. For each new element, if it's larger than the last element in dp, it extends the longest subsequence. Otherwise, we use binary search to find the position where it can replace an element, keeping the array optimal for future extensions.
dp with the first element.nums[i]:nums[i] is greater than dp[-1], append it to dp.dp where dp[pos] >= nums[i] using binary search, and replace dp[pos] with nums[i].dp is the answer.from bisect import bisect_left
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = []
dp.append(nums[0])
LIS = 1
for i in range(1, len(nums)):
if dp[-1] < nums[i]:
dp.append(nums[i])
LIS += 1
continue
idx = bisect_left(dp, nums[i])
dp[idx] = nums[i]
return LISIn the binary search approach, the dp array at the end does not contain the actual longest increasing subsequence. It contains the smallest tail elements for subsequences of each length. The length of this array is the LIS length, but the elements themselves may not form a valid increasing subsequence from the original array.
When finding the position to replace in the dp array, you need to find the leftmost position where dp[pos] >= nums[i]. Using > instead of >= can lead to duplicate values being treated as increasing, which violates the strictly increasing requirement.
In the O(n^2) DP approach, the answer is the maximum value across all dp[i], not just dp[n-1]. The longest increasing subsequence might end at any index, not necessarily the last one. Returning only the last element of the DP array misses cases where the LIS ends earlier in the array.