目录
300.最长递增子序列
今天开始正式子序列系列,本题是比较简单的,感受感受一下子序列题目的思路。
首先要明确的就是dp数组的意义,dp[i]代表以nums[i]为结尾的递增子序列的最大长度,只有这样每次比较nums[i]和nums[j]的大小才会有意义(看是否能在以nums[j]为结尾的子序列中加入nums[i]),这个超级重要。明确了这一点,要理解需要两层嵌套遍历就容易一点,要依次更新每一个dp[i],就必须每次都遍历一整遍前面的nums,一旦发现有小于当前nums[i]的,就证明当前nums[i]可以作为原来(以nums[j]为结尾的)子序列的新结尾,并且试图更新dp[i]的值,注意外层遍历从下标1开始(0前面没有元素,不可以用来更新dp[0]),内层循环从0开始到 i 前结束(遍历所有可能成为 以nums[i]结尾子序列的倒数第二个值 的数)。默认最小子序列为1,所以自然不必说dp数组需要全部初始化为1。最后,因为最长子序列的结尾元素不一定是nums最后一个元素,所以需要另设置res来实时更新最长子序列长度。
int lengthOfLIS(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
return result;
}
674. 最长连续递增序列
本题相对于昨天的动态规划:300.最长递增子序列 最大的区别在于“连续”。 先尝试自己做做,感受一下区别
视频讲解:动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列_哔哩哔哩_bilibili
这道题是求连续增加的子序列的最大长度,比上一题简单一些。
贪心算法——
因为是连续增加,所以只要判断当前nums值大于前一个,就size++,而一旦后一个不大于前一个,那就证明这一个连续递增子序列结束、新的子序列开始,所以需要回正size为初始值1,每次更新了size就要同步试图更新res。
int findLengthOfLCIS(vector<int>& nums) {
int size=1,res=1;
for(int i=1;i<nums.size();i++){
if(nums[i]>nums[i-1]) size++;
else size=1;
if(size>res) res=size;
}
return res;
}
动态规划——
这回的dp[i]意味着以nums[i]为结尾的最长 连续 递增子序列长度,因为连续所以以nums[i]为结尾的子序列的前一个值必然是nums[i-1],故而不用再开一个内层循环来遍历谁可能是 最长以nums[i]结尾的子序列 的前一个值。其他一样。
int findLengthOfLCIS(vector<int>& nums) {
vector<int> dp(nums.size(),1);
int res=1;
for(int i=1;i<nums.size();i++){
if(nums[i]>nums[i-1]) dp[i]=dp[i-1]+1;
if(dp[i]>res) res=dp[i];
}
return res;
}
718. 最长重复子数组
稍有难度,要使用二维dp数组了
视频讲解:动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组_哔哩哔哩_bilibili
这道题需要使用二维数组来记录情况,这是因为dp[i][j]代表以nums1[i]/nums2[j]为结尾的相同子数组的最大长度,题目考察到两个数组,一旦遇到相等的元素,需要同时查看两个数组的上一个元素的情况(在dp[i-1][j-1]的值上加一),而使用二维数组来记录就刚好。文章选择开辟数组长度加一的二维数组,空出dp[0][]、dp[][0]这一行列不赋予意义,而是用dp[i+1][j+1]代表以nums[i]为结尾的最长相同子数组长度,这是因为如果第一个相等的元素恰好就是数组的第一个值的话,就没法使用递推公式来赋值了(nums[0]前面没有值,用来在此基础上加一计算dp[0]),需要特别处理,除非在前面多设置一行列赋值为0。
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
这道题也可以使用滚动数组的方式来减少空间的消耗,这是因为要求的是相等的子数组的长度,所以也可以就着某一个数组来比较,当然还是不能避免遍历两个数组,但是空间可以重叠。这时dp[j+1]就表示以nums2[j]为结尾的相同子数组的最大长度,递推公式变为dp[j+1]=dp[j]+1,还是要都初始化为0,道理不变。但是因为递推dp[j+1]的时候要用到dp[i][j]的元素(见下图),所以前一列的值是不能先变的,也就是dp[j]不能比dp[j+1]先改变。还有一点就是一旦遇到两个值不相等了,那就要立即设置dp[j+1]=0,之前使用二维数组的时候不用是因为递推dp[i][j]的时候,直接利用坐上角的值,如果这个i、j下标对应的两个值不等的话,是不会更新的(也就是还是初始化的0),而要使用一维数组的话就不一定了,如果不及时清零,那么这个数只是历史值,而不会代表dp[i-1][j-1]的值,就不满足递推公式了,而且放心你的res是及时同步更新的不会错过每一个不为0的值。
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<int> dp(vector<int>(nums2.size()+1,0));
int res=0;
for(int i=0;i<nums1.size();i++){
for(int j=nums2.size()-1;j>=0;j--){
if(nums1[i] == nums2[j]){
dp[j+1]=dp[j]+1;
}else dp[j+1]=0;
if(dp[j+1]>res) res=dp[j+1];
}
}
return res;
}
如果使用dp[i][j]来代表以nums1[i]/nums2[j]为结尾的相同子数组的最大长度的话,需要特别处理两个数组第一个值就相同的情况,且因为可能存在这种情况,需要试图更新res的值,而这也就决定了不能在遍历两个数组的时候(更新其他dp数组的值的时候)不遍历下标为0的元素,或者说选择在初始化dp[0]的时候就直接判断res是否能为1,总之不能不考虑0下标的相等情况。
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
// 要对第一行,第一列经行初始化
for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1;
for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1;
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}