300.最长递增子序列
问题:给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,
[3,6,2,7]
是数组[0,3,1,6,2,2,7]
的子序列。
思路:
- 动态规划
第一步,定义dp数组的含义。定义dp[i]
表示以第i
个元素结尾的最长递增子序列的长度。
第二步,确定状态转移方程。假设需要求以第i
个元素结尾的最长递增子序列的长度,即dp[i]。此时我们需要找到前i
个元素中,对应元素值小于第i个元素值,且以这个元素结尾的最长递增子序列长度最大,再在这个子序列末尾加上第i
个元素nums[i]
。我们可以写出这样的状态转移方程。
d
p
[
i
]
=
m
a
x
(
d
p
[
j
]
)
+
1
,
0
≤
j
≤
i
a
n
d
n
u
m
s
[
i
]
>
n
u
m
s
[
j
]
dp[i]=max(dp[j])+1, \quad 0\leq j \leq i \quad and \quad nums[i] > nums[j]
dp[i]=max(dp[j])+1,0≤j≤iandnums[i]>nums[j]
第三步,初始化dp
数组,dp[0]=1
,这个很容易知道,另外,由于我只会在nums[i] > nums[j]
时更新dp[i]
的值,所以再寻找最大的dp[j]
时,会先令dp[i]=1
,若前i
个元素中不存在递增的子序列,dp[i]=1
,若有则会更新dp[i]
的值。防止出现dp[i]=0
的情况。
最终所求的结果为 m a x ( d p [ i ] ) 0 ≤ i ≤ n u m s . l e n g t h max(dp[i]) \quad 0\leq i \leq nums.length max(dp[i])0≤i≤nums.length.
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if(nums.length == 0 || nums.length == 1) return nums.length;
int[] dp = new int[len];
int max = 0;
dp[0] = 1;
for(int i = 1; i < len; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
- 贪心 + 二分查找
贪心策略:若我们想要找到的递增子序列尽可能长,那么我们需要让每次递增的幅度尽可能小。
因此使用dp[i]
表示长度为i的最长递增子序列末尾元素最小值。使用len记录目前最长递增子序列的长度。
接下来证明dp
数组的单调性,反证法:假设有dp[j] > dp[i]
且 j < i
。将长度为i
的最长递增子序列的后i-j
个元素删去,我们可以得到一个长度为j的最长递增子序列,则必有该子序列的第j个元素小于dp[i]
,根据假设,也小于dp[j]
。这样我们就找到了一个长度为j
的最长递增子序列,且其末尾元素比dp[j]
小,这与我们定义的dp
数组含义相矛盾,所以dp
数组应该是单调递增的。最后得到的dp
数组其实就是所求的最长递增子序列。
算法流程:我们需要从前往后的遍历数组nums
,假设当前元素为nums[i]
,当前的最长递增子序列长度为len
。则有两种情况
- 若
nums[i] > dp[len]
,则直接将nums[i]
添加到dp
数组的末尾,同时len
的长度加1
. - 若
nums[i] <= dp[len]
,从后往前遍历当前dp
数组,找到第一个小于nums[i]
的元素dp[j]
,并更新dp[j+1] = nums[i]
。由于dp
数组是单调的,所以可以使用二分查找来优化时间复杂度。
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0 || nums.length == 1) return nums.length;
int len = 1;
int[] dp = new int[nums.length + 1];
dp[1] = nums[0];
for(int i = 1; i < nums.length; i++){
if(nums[i] > dp[len]){
dp[++len] = nums[i];
} else {
int l = 1, r = len, pos = 0;//若所有元素都大于nums[i],则更新dp数组的第一个元素
while(l <= r){
int mid = l + (int) (r - l) / 2;
if(dp[mid] < nums[i]){
pos = mid;
l = mid + 1;
} else{
r = mid - 1;
}
}
dp[pos + 1] = nums[i];
}
}
return len;
}
}
另外,我觉得二分查找需要注意的两个细节
- while循环结束的条件,我们假设搜索区间是一个长度为len的数组
l <= r
,表示整个搜索区间是一个闭区间[l , r]
,此时初始的右边界应该为len - 1,是数组最后一个元素的索引l < r
,表示整个搜索区间是一个左闭右开区间[l , r)
,此时初始的右边界应该是len, 即索引为len是越界的。
- 每次搜索区间的边界变化
- 因为这道题目里面,每次搜索前,已经判断过mid位置对应的元素了,下一次搜索时,应该将mid从搜索区间去掉。所以这里是
l = mid + 1
和r = mid - 1
- 因为这道题目里面,每次搜索前,已经判断过mid位置对应的元素了,下一次搜索时,应该将mid从搜索区间去掉。所以这里是
参考官方题解总结的思路,若哪里理解有误,望指正。欢迎讨论。