代码随想录第四十八天|1143.最长公共子序列 1035.不相交的线 53. 最大子序和 392.判断子序列

1143.最长公共子序列:

文档讲解:代码随想录|1143.最长公共子序列

视频讲解:https://www.bilibili.com/video/BV1ye4y1L7CQ

状态:已做出

一、题目要求:

题目要求在给定的字符串text1和text2中找到公共最长子序列,返回其长度。

二、出现的错误:

在推导公式上出现了问题,没有正确处理遍历的两个字符不相等的情况,将左上角的元素直接赋值给了dp[i][j],但是左上角的值不可能是最佳值,因为dp[i][j-1]和dp[i-1][j]都包含了这个元素,正确的要求是找dp[i-1][j]和dp[i][j-1]的最大值。

三、解题思路:

首先第一步是确定dp数组的含义,这道题目使用一个二维数组dp[i][j],i用来记录text1的元素下标,j用来记录text2的元素下标,题目不要求子序列连续,那么dp的含义就和最长重复子数组的dp含义有所区别,最长重复子数组要求连续,在连续的情况下dp只需要靠前面一个元素推导,所以dp元素是把当前元素作为子序列最后一个的最大长度,这道题目不要求连续,这里dp[i][j]设计为text1[0~i-1]text2[0~j-1]区间最大公共子序列的长度,dp为了避免进行多于的初始化,所以dp行和列都加一层,所以这里i和j在字符串里对应的是i-1和j-1。第二步确定递推公式,根据dp含义可知,既然dp保存的是最大值,在text1[i-1]等于text2[j-1]时,直接赋值为左上角的值dp[i-1][j-1],因为要去掉text1[i]和text2[j]的元素,当不等于时,dp[i][j]取dp[i-1][j]和dp[i][j-1]的最大值,就是取二维数组里的上一行元素或者上一列元素,因为要让不相等的两个元素取找前面的元素看是否有相等的来推导出来,这样整个推导公式就推出来了。第三步是初始化dp数组,这里因为已经使用额外的一层来对text1[0]和text2[0]对应的行和列进行处理了,所以只需要在一开始设置默认值都为0既可。第四步是确定遍历顺序,根据推导公式可以知道,我们需要使用一个嵌套循环分别遍历text1和text2的元素,这两者可以交换内外顺序,并且两个循环需要从前往后遍历,因为dp需要左上角、上面和左边这三个元素推导。最后一步举例出dp元素既可。

四、代码实现:

cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n=text1.size();  //变量n保存text1的元素个数
        int m=text2.size();  //变量m保存text2的元素个数
        
        //设置二维数组dp,这里多设置一行一列,避免额外对第零行和第零列初始化,这样设置后第零行和第零列就完全是为推导后面dp元素服务
        vector<vector<int>>dp(n+1, vector<int>(m+1,0));
        
        //内外两层可以交换,不影响最后的结果,都从一开始遍历,零不做处理
        for(int i=1;i<=n;++i) {
            for(int j=1;j<=m;++j) {
                //这里dp里的i对应的是text1[i-1],j对应的是text2[j-1],因为dp的i和j在零时不对应任何元素
                if(text1[i-1]==text2[j-1])
                    dp[i][j]=dp[i-1][j-1]+1;  //取左上角的元素
                else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                /* 因为dp元素保存的是最大值,text1[i-1]不等于text2[j-1],那么就有两种情况,一种
                ** 是去掉当前text1[i-1],取text1[0~i-2]和text2[0~j-1]区间的最大公共子序列,
                ** 一种是去掉text2[j-1],取text1[0~i-1]和text2[0~j-2]区间的最大公共子序列
                ** 两种情况取最大值,对应dp就是dp[i-1][j]和dp[i][j-1]
                */
            }
        }
        
        //因为dp元素保存的是最大值,所以在最后只要返回最右下角的元素即可得到最大长度
        return dp[n][m];
    }
};

五、代码复杂度:

时间复杂度:进行嵌套循环,为O(n*m),n为text1的元素个数,m为text2的元素个数。

空间复杂度:创建了一个二维数组,为O(n*m)。

六、收获:

通过这道题目的练习让我对这类找子序列的二维进阶有了一定的了解,一开始和求连续子数组的推导公式弄混淆了,不相等的情况下也等于由左上角推导,一开始发现这个错误我还是不知道为什么不能左上角来推导,后来结合dp含义才真正想清楚,因为dp是保存的最大值,所以dp[i][j-1]和dp[i-1][j]本身就包含有dp[i-1][j-1],所以要用上面和左边的值推导。

1035.不相交的线:

文档讲解:代码随想录|1035.不相交的线

视频讲解:https://www.bilibili.com/video/BV1h84y1x7MP

状态:已做出

一、题目要求:

这道题目要求两个给定的数组一个在上一个在下,对其相同的元素进行连线,要求连接的线不能相交,求出最多能连接多少条线。

二、出现的错误:

这道题目我一直在尝试使用一维数组来解决,当时一直出错,主要是因为推导公式上出现了错误,一维数组推导公式是最难理解的点,当时看了很久标准代码才理解其中的含义,我出错是因为没有正确的指认出前一个dp元素,而是跳过中间的一部分dp元素,因为我在设置推导公式里把两个连接断电的较小指作为当前dp的推到因素,导致忽略了中间的dp元素,最后推导出了错误的值。

三、解题思路:

第一步是设置dp数组和确定dp数组的含义,这里设置的是一维数组,dp长度既可以设置为nums1.size()+1也可以设置为nums2.size()+1,dp数组的含义就是在[0~i-1]区间的最大连接个数为dp[i]。第二部确定推导公式,这一步是整个代码里最难想的,虽然dp含义很简单,就是保存最大值,但是要找寻的数组却有两个,怎么去把以为数组dp和两个维度的nums数组结合起来就是关键,在文档里面使用的是二维数组,这道题目使用二维数组代码和上一道题目一模一样,只要把字符串换成数组既可,这里利用二维数组的dp元素来对应一维数组的dp元素,我的代码dp的长度设计为nums2.size()+1,这里就用j来表示dp和nums2数组的元素位置,遍历还是使用嵌套循环,这里循环里面需要设置一个prev变量来保存上一个已遍历的dp元素,当nums1[i]等于nums2[j]时,赋值为prev+1,这里相当与二维数组里的dp[i][j]=dp[i-1][j-1]+1,就是取左上角的值,当不相等时,推导公式为dp[j+1]=max(dp[j+1],dp[j]),这里使用dp[j+1]是因为一开始设置的dp[0]本身不纳入计算,所以dp[j+1]对应nums2[j],其中dp[j+1]=dp[j+1]相当于二维数组里的dp[i][j]=dp[i-1][j],因为这里的dp[j+1]在当前i处还没处理,那么此时保存的就是i-1时的dp[j+1],dp[j+1]=dp[j]相当于二维数组的dp[i][j]=dp[i][j-1],因为dp[j]在当前i时已经处理过了,最后在不相等的时候推导公式为dp[j+1]=max(dp[j+1],dp[j])。第三步初始化化dp数组,这里也是直接设置默认值为零既可。第四步确定遍历顺序,这里的遍历顺序和上一题一样,都是嵌套循环,内外分别遍历nums1和nums2数组。最后举例出dp元数据既可。

四、代码实现:

cpp 复制代码
class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        
        //设置一维数组dp,这里长度设置为m+1,把二维数组里的n+1维度给去掉了,这里去掉m+1也可以实现
        vector<int> dp(m + 1, 0);
        
        /* 内外循环不可以交换位置,以下是我对不能交换的理解(以二维数组来做对照):
        ** 在二维数组里一个dp元素需要靠左上角、左边和上面的元素推导,
        ** 这里一维数组看作把二维数组的列给优化了,只保留了行,实行滚动数组
        ** 在一维数组的循环里使用了一个prev来保存左上角的值,在当前遍历到的dp[j+1]还没有处理
        ** 处理的时候,其值就是上一行的dp[j+1],prev最后保存这个位置的值,就相当于是保存的
        ** 左上角的值,如果先遍历j,就是选遍历列,prev会变成保留的是正上方的元素。
        ** 这里我想到一个点,就是如果这道题目不需要左上角的值来进行推导,那么循环就可以进行
        ** 交换,为什么?因为先遍历列的话,最后一列就是的dp元素就是这一列的最大值,dp元素
        ** 本身就需要左边的最大值和正上方的值来推导,如果一维数组dp前一个元素保存的就是最大值
        ** 那么最后得到的值就不会改变,只是中途出现的元素和二维数组的元素有区别。
        */
        for(int i = 0; i < n; i++) {
            int prev = 0;  //关键:保存左上角的值
            for(int j = 0; j < m; j++) {
                int temp = dp[j + 1];  //保存当前值,当前dp[j+1]没有处理过,就相当于是上一行的元素,这里就需要先保留,在dp[j+1]处理后再把它赋值给prev,为后面的dp服务
                
                if(nums1[i] == nums2[j]) {
                    dp[j + 1] = prev + 1;  //相当于dp[i+1][j+1] = dp[i][j] + 1
                } else {
                    //dp[j+1] = dp[j+1]相当于dp[i+1][j+1] = dp[i][j+1]
                    //dp[j+1] = dp[j]相当于dp[i+1][j+1] = dp[i+1][j]
                    dp[j + 1] = max(dp[j + 1], dp[j]);
                }
                
                prev = temp;  //更新prev
            }
        }
        
        return dp[m];
    }
};

五、代码复杂度:

时间复杂度:嵌套循环,为O(n*m),n为nums1.size(),m为nums2.size()。

空间复杂度:创建了一个一维数组,为O(m)。

六、收获:

当时做这道题目的时候没有想到这道题目和上一题一样,看了题目只是感觉能用一维数组来解,没想到二维数组与一维数组的关系,所以一直出错,后来看了一维数组的解法,就是二维数组的进阶,把其中一个维度去掉,实行类似滚动数组的形式来求所有do元素,之所以能实现滚动数组,就是因为二维数组的dp元素是上一行或者上一列推导出来的,为什么代码里面要多使用一个变量prev呢?因为在二维数组里,左上角的值在一维数组里会被覆盖,必须要使用一个变量来保存左上角的值,避免被覆盖找不到。通过这一系列的思考,对找子序列这类题目有了更加系统的认识,做一维数组感觉能进一步理解二维数组的含义,并且也能进一步优化空间,让代码更加完美。

53. 最大子序和:

文档讲解:代码随想录|53.最大子序和

视频讲解:https://www.bilibili.com/video/BV19V4y1F7b5

状态:已做出

一、题目要求:

在给定的数组nums里找到一个元素和最大的子数组,返回这个子数组的元素和。

二、难点:

这道题目主要的难点体现在dp和其推导公式的设置上,首先是dp的设置上,这道题目不能单纯的把dp[i]的含义设置为[0~i]区间的最大子数组和,因为子数组是存在连续性的,如果dp[i]为最大值就会失去连续性这个特征,比如在[-2,1,-3,4,-1,2,1,-5,4]这个nums数组里,dp保存最大值,那么dp[3]就是5,这里就是跳过了中间的元素,直接把nums[1]和nums[3]相加了,所以dp[i]必须设置为把nums[i]最为最后一个位置下的最大子数组和。这里推导公式容易出现错误,这里每次需要判断dp[i-1]是否为零,不能单纯判断nums[i-1]是否为零,因为dp表示的是最大子数组,而nums[i-1]只能表示一个元素。

三、解题思路:

第一部确定dp含义,dp[i]为将nums[i]作为子数组最后一个位置的情况下,最大子数组和为dp[i]。第二步是确定推导公式,当dp[i-1]小于零时,说明此时nums[i]相加前面的元素必定会让子数组和变小,所以此时dp[i]直接赋值为nums[i],当dp[i-1]不小于零时,dp[i]=nums[i]+dp[i-1],这里就能保证nums[i]考虑进去的时候相加上前面的最大子数组和使其大于nums[i]。第三步初始化dp数组,这里不需要额外对dp数组设置默认值,因为根据推导公式可以看出,dp元素是直接被赋值,但是dp元素需要靠前一个元素推导,所以要对dp[0]进行初始化,初始化为nums[0]既可。第三步是确定遍历顺序,这里从推到公式可以得出,只需要遍历一遍nums数组元素既可,并且从前往后遍历。最后举列出dp元素既可。还有一个点需要注意,因为dp元素是将nums[i]作为最后一个的情况下的最大子数组和,所以最后返回值并不是dp[n-1],而是需要设置一个遍历,把do元素里出现的最大值保存起来在最后返回这个变量才能返回正确的最大值。

四、代码实现:

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n=nums.size();
        
        vector<int>dp(n,0);  //创建一维数组dp,默认值都为零
        
        //初始化dp[0],dp的含义是将nums[i]作为最后一个位置,所以这里初始化为nums[0]
        dp[0]=nums[0];
        int res=dp[0];  //设置一个变量res用来保存dp里最大的元素,最后返回这个变量即可
        
        for(int i=1;i<n;++i) {
            //当发现dp的前一个元素为负数时,nums[i]加上总和一定会变小,所以此时不能加上dp[i-1]
            if(dp[i-1]<0) dp[i]=nums[i];
            else dp[i]=nums[i]+dp[i-1];  //想要求最大子数组和,相加的值一定要是非负数
            res=max(res,dp[i]);
        }
        
        return res;
    }
};

五、代码复杂度:

时间复杂度:遍历了一遍nums元素,所以为O(n),n为nums的元素个数。

空间复杂度:创建了一个一维数组,所以为O(n)。

六、收获:

这道题目的dp含义和之前的一道题目很相似,这道题目有一个新颖的特点,就是要判断前一个dp元素是否小于零,因为这道题目需要的是最大子数组和,这里就有两个要求,一个是元素必须要连续,并且是最大和,一旦加上负数和一定会变小,所以一定要避免加上负数,这样最后得到的一定是相对最大和。之前这道题目使用的是贪心,这次使用动规,能更加直观的明白这道题目的核心特点,对动规的dp和推到公式的设置更加熟练了。

392.判断子序列:

文档讲解:代码随想录|392.判断子序列

视频讲解:https://www.bilibili.com/video/BV1tv4y1B7ym/

状态:已做出

一、题目要求:

给定一个字符串s和字符串t,判断s是否为t的子序列,这里放子序列不要求连续,只需要字符前后顺序不变。

二、解题思路:

这道题目我自己的代码严格意义上来说不是动规做法,实际上是利用一维数组实现的双指针做法,最后只是看了文档的二维数组动规做法,自己没有实际去做动规了,这里利用一个一维数组dp,此数组的元素个数设置为s字符串的长度,dp的每个元素相当于是一个指针移动,然后使用一个循环遍历字符串t,这个循环相当于另一个指针,这样两个双指针分别在两个字符串里遍历,每当判断出s[i]==t[j]时,s的指针向后移动一位,t值主要移动指针,不管什么情况,遍历后都要移动一位,最后dp[s.size()-1]得到的就是最终结果。文档的二维数组动规解法大概是靠左上角和左边位置的元素推导,因为这里是判断s是否为t的子序列,所以主循环是遍历字符串s,在两个字符不相同的情况下,dp[i][j]保存同一行前一个元素,如果dp[i][j]=dp[i-1][j],那么就相当于是同一个t元素去找s里是否有相等的字符,这样就是在判断字符串t是否为字符串s的子序列了。在最后得到的dp[s.size()][t.size()]还需要判断长度是否等于s.size(),相等就说明s是t的子序列。

三、代码实现:

双指针:

cpp 复制代码
class Solution {
public:
    bool isSubsequence(string s, string t) {
        int n=s.size();
        int m=t.size();
        
        if(n>m) return false;
        if(n==0) return true;  //空串一定是t的子序列
        
        vector<bool>dp(n,false);  //这里设置的是一个bool类型的dp数组
        int dix=0;   //这个变量用来遍历字符串s
        
        for(int i=0;i<m && dix<n;++i) {
            if(t[i]==s[dix]) dp[dix++]=true;  //双指针的核心
        }
        
        return dp[n-1];
    }
};

四、代码复杂度:

时间复杂度:双指针只用到了一个循环,为O(n),n为s.size()。

空间复杂度:创建了一个一维数组dp,为O(n)。

五、收获:

这道题目的动规主要是看的文档上的解法,自己实际没去做,因为当时没意识到自己使用的是双指针,后来才想到这种做法dp元素根本不需要靠前面的dp元素来推导才意识到这种一维数组的做法本质还是双指针,不过也让我了解到了动规个非动规明显的区别,只要发现设置的dp数组能不能通过前面的dp元素推导就一定不是动规解法。这道题目文档里说是编辑距离的入门题目,但代码和之前做的二维数组动规区别目前看没什么太大区别,在后续相关的问题上再去发现更多的区别。

相关推荐
AI妈妈手把手3 小时前
YOLO V2全面解析:更快、更准、更强大的目标检测算法
人工智能·算法·yolo·目标检测·计算机视觉·yolo v2
极客智造3 小时前
编程世界的内在逻辑:深入探索数据结构、算法复杂度与抽象数据类型
数据结构·算法·数学建模
好好学习啊天天向上3 小时前
多维c++ vector, vector<pair<int,int>>, vector<vector<pair<int,int>>>示例
开发语言·c++·算法
MicroTech20254 小时前
MLGO微算法科技 LOP算法:实现多用户无线传感系统中边缘协同AI推理的智能优化路径
人工智能·科技·算法
Greedy Alg4 小时前
Leetcode 279. 完全平方数
算法
剪一朵云爱着4 小时前
力扣410. 分割数组的最大值
算法·leetcode
Swift社区4 小时前
LeetCode 410 - 分割数组的最大值
算法·leetcode·职场和发展
ゞ 正在缓冲99%…4 小时前
leetcode375.猜数字大小II
数据结构·算法·leetcode·动态规划
Greedy Alg4 小时前
LeetCode 79. 单词搜索
算法