Tag
: Dynamic Programming
Binary Search
Monotonic Stack
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
Example 3:
Input: nums = [7,7,7,7,7,7,7]
Output: 1
Constraints:
$1 \le nums.length \le 2500$ $-10^4 \le nums[i] \le 10^4$
This problem has two important attributes that let us know it should be solved by dynamic programming:
- The question is asking for the maximum or minimum of something
- We have to make decisions that may depend on previously made decisions, which is very typical of a problem involving subsequences.
As we go through the input, each "decision" we must make is simple: is it worth it to consider this number? If we use a number, it may contribute towards an increasing subsequence, but it may also eliminate larger elements that came before it. For example, let's say we have nums = [5, 6, 7, 8, 1, 2, 3]
. It isn't worth using the 1, 2, or 3, since using any of them would eliminate 5, 6, 7, and 8, which form the longest increasing subsequence. We can use dynamic programming to determine whether an element is worth using or not.
Typically, dynamic programming problems can be solved with three main components. If you're new to dynamic programming, this might be hard to understand but is extremely valuable to learn since most dynamic programming problems can be solved this way.
First, we need some function or array that represents the answer to the problem from a given state. For many solutions on LeetCode, you will see this function/array named "dp". For this problem, let's say that we have an array dp
. As just stated, this array needs to represent the answer to the problem for a given state, so let's say that dp[i]
represents the length of the longest increasing subsequence that ends with the ith element. The "state" is one-dimensional since it can be represented with only one variable - the index i
.
Second, we need a way to transition between states, such as dp[5]
and dp[7]
. This is called a recurrence relation and can sometimes be tricky to figure out. Let's say we know dp[0]
, dp[1]
, and dp[2]
. How can we find dp[3]
given this information? Well, since dp[2]
represents the length of the longest increasing subsequence that ends with nums[2]
, if nums[3] > nums[2]
, then we can simply take the subsequence ending at i = 2
and append nums[3]
to it, increasing the length by 1
. The same can be said for nums[0]
and nums[1]
if nums[3]
is larger. Of course, we should try to maximize dp[3]
, so we need to check all 3. Formally, the recurrence relation is: dp[i] = max(dp[j] + 1)
for all j
where nums[j] < nums[i]
and j < i
.
The third component is the simplest: we need a base case. For this problem, we can initialize every element of dp to 1, since every element on its own is technically an increasing subsequence.
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
# Top-Down DP (Recursion)
n = len(nums)
memo = [-1] * (n + 1)
def dp(prev, curr):
# Base case
if curr == len(nums):
return 0
if memo[prev + 1] != -1:
return memo[prev + 1]
# Recurrence relation
to_take_next_element = 0
if prev == -1 or nums[prev] < nums[curr]:
to_take_next_element = 1 + dp(curr, curr + 1)
not_to_take_next_element = dp(prev, curr + 1)
memo[prev + 1] = max(to_take_next_element, not_to_take_next_element)
return memo[prev + 1]
return dp(-1, 0)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
lis = 0
n = len(nums)
memo = [-1] * (n + 1)
def dp(curr, prev):
# Base case
if curr == n:
return 0
if memo[prev + 1] != -1:
return memo[prev + 1]
# Recurrence relation: to take or to skip the next element?
# To skip: move on to the next number
skip = dp(curr + 1, prev)
# To take: update previous value in the with current value, move on to the next number
take = 0
# Check if subsequence is empty or the previous value in the subsequence is strictly smaller than current number
if prev == -1 or nums[prev] < nums[curr]:
take = 1 + dp(curr + 1, curr)
memo[prev + 1] = max(take, skip)
return memo[prev + 1]
return dp(0, -1)
-
Initialize an array
dp
with lengthnums.length
and all elements equal to 1.dp[i]
represents the length of the longest increasing subsequence that ends with the element at indexi
. -
Iterate from
i = 1
toi = nums.length - 1
. At each iteration, use a second for loop to iterate fromj = 0
toj = i - 1
(all the elements beforei
). For each element beforei
, check if that element is smaller thannums[i]
. If so, setdp[i] = max(dp[i], dp[j] + 1)
. -
Return the
max
value fromdp
.
Time Complexity: O(N^2)
, nested loop through the entire input array in the worst case
Space Complexity: O(N)
, subarray can be as large in size as the input array if every element in input array is strictly increasing
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
# Dynamic Programming
n = len(nums)
dp = [1] * n
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
dp = [1 for _ in range(n)]
for i in range(1, n) :
for j in range(i) :
if nums[j] < nums[i] and dp[i] < dp[j] + 1 :
dp[i] = dp[j] + 1
return max(dp)
It appears the best way to build an increasing subsequence is: for each element num
, if num
is greater than the largest element in our subsequence
, then add it to the subsequence
. Otherwise, perform a linear scan through the subsequence
starting from the smallest element and replace the first element that is greater than or equal to num
with num
. This opens the door for elements that are greater than num
but less than the element replaced to be included in the sequence.
One thing to add: this algorithm does not always generate a valid subsequence of the input, but the length of the subsequence
will always equal the length of the longest increasing subsequence. For example, with the input [3, 4, 5, 1]
, at the end we will have longest_increasing_subsequence = [1, 4, 5]
, which isn't a subsequence, but the length is still correct. The length remains correct because the length only changes when a new element is larger than any element in the subsequence. In that case, the element is appended to the subsequence instead of replacing an existing element.
Time Complexity: O(N^2)
, nested loop through the entire input array in the worst case
Space Complexity: O(N)
, subarray can be as large in size as the input array if every element in input array is strictly increasing
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
pivot = nums[0]
longest_increasing_subsequence = list()
longest_increasing_subsequence.append(pivot)
for num in nums[1:]:
idx = 0
# Check if the consequence is not increasing with the current numbe
if num <= longest_increasing_subsequence[-1]:
# Search for the candidate to keep in the subsequence
while longest_increasing_subsequence[idx] < num:
idx += 1
longest_increasing_subsequence[idx] = num
# Check if the subsequence is increasing, or the next number is bigger than the newest number in the subsequence
else:
longest_increasing_subsequence.append(num)
return len(longest_increasing_subsequence)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
lis = list()
lis.append(nums[0])
for num in nums[1:]:
i = 0
if lis[-1] < num:
lis.append(num)
else:
while lis[i] < num:
i += 1
lis[i] = num
return len(lis)
In the previous approach, when we have an element num
that is not greater than all the elements in lis
, we perform a linear scan to find the first element in lis
that is greater than or equal to num
. Since lis
is in sorted order, we can use binary search instead to greatly improve the efficiency of our algorithm.
Algorithm
- Initialize an array
lis
which contains the first element ofnums
. - Iterate through the input, starting from the second element. For each element
num
:
- If
num
is greater than any element inlis
, then addnum
tolis
. - Otherwise, perform a binary search in
lis
to find the smallest element that is greater than or equal tonum
. Replace that element withnum
.
- Return the length of
lis
.
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
lis = list()
for num in nums:
lo, hi = 0, len(lis)
while lo < hi:
mi = lo + (hi - lo) // 2
if lis[mi] < num:
lo = mi + 1
else:
hi = mi
i = lo
if i == len(lis):
lis.append(num)
else:
lis[i] = num
return len(lis)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
lis = list()
for num in nums:
i = bisect.bisect_left(lis, num)
if i == len(lis):
lis.append(num)
else:
lis[i] = num
return len(lis)