代码随想录算法训练营第44天 | LeetCode1143.最长公共子序列、 LeetCode1035.不相交的线、LeetCode53.最大子数组和、LeetCode392.判断子序列

目录

LeetCode1143.最长公共子序列

LeetCode1035.不相交的线

LeetCode53.最大子数组和 

1. 贪心算法

2. 动态规划

LeetCode392.判断子序列

1. 双指针法

2. 动态规划


LeetCode1143.最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

思路:昨天讲了一个最长重复子数组,求的是连续的子序列。

而这道题求的是最长公共子序列,也就是说序列是非连续的。

但是我们在这里处理的时候大体还是和最长重复子数组是一样的,只是在text1[i-1]和text2[j-1]不相等时,需要取dp[i-1][j]以及dp[i][j-1]的最大值(对于dp[i][j-1]表示以下标i-1结束的text1以及j-2结束的text2的最长公共子序列的长度,同理,dp[i-1][j]也能知道是以下标i-2结束的text1以及j-1结束的text2的最长公共子序列的长度),这里是将前面的状态一直保持下去,这样能够在最后的时候返回最长子序列长度。

最长重复子串的时候不需要记录是因为连续这个条件的限制,只要下个元素不等,计数就中断了,需要从下一个相同的元素开始计数;

而这个最长公共子序列却是要记住前面的状态,因为没有连续这个限制,所以在不等的时候需要将前面最长的子序列长度记下来。

    int longestCommonSubsequence(string text1, string text2) {
        //dp[i][j]表示以下标i-1结束的text1以及j-1结束的text2的最长公共子序列的长度
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for(int i = 1; i < text1.size() + 1; i ++){
            for(int j = 1; j < text2.size() + 1; 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]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }

时间复杂度:O(n*m)

空间复杂度:O(n*m)

LeetCode1035.不相交的线

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  •  nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

思路:上面的题搞清楚后,这道题也就非常简单了。

几乎一模一样,本质都是求的最长公共子序列。

    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        for(int i = 1; i < nums1.size() + 1; i ++){
            for(int j = 1; j < nums2.size() + 1; j ++){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[nums1.size()][nums2.size()];
    }

时间复杂度:O(n*m)

空间复杂度:O(n*m)

LeetCode53.最大子数组和 

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。 

思路:之前贪心系列的时候做过这道题,这里还是再讲一下,同时尝试使用动态规划来做。

1. 贪心算法

这里需要的是连续的子数组,所以我们的局部最优就是当和小于0时,这时候没有必要再加下去了,因为负的加上正的可能还是负的,所以直接在这个时候重新开始计和,从下一个数开始重新计。同时也需要对最终结果result不断更新,最后的全局最优就是子数组的和最大。

这里需要注意一下在for循环里面的代码顺序,因为顺序不对可能需要多写一点代码,更新result的代码应该在count<0判断之前,保证能够在数组nums都为负数的时候取最大的负数返回,而不是说返回0或者无解。同样result的初始值也有讲究,不要设为0,因为当nums全为负数时,根本不会更新result,这时就会返回0,很明显就是错误答案。

所以需要注意一下for循环里面的顺序,还有result的初始值设置。

    int maxSubArray(vector<int>& nums) {
        int count = 0;//累加和
        int result = INT_MIN;//记录最终结果
        for(int i = 0; i < nums.size(); i ++){
            count += nums[i];
            if(count > result) result = count;//及时更新最大值
            if(count < 0) count = 0;//重新开始计和
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

当然还可以使用动态规划来做,dp[i]为前i个元素的最大子数组和,所以它有两种更新方式,要么加入前面dp[i-1],要么自己单独重新作为起点重新开始计和,当然,两者需要取最大的一种情况。

    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);//dp[i]记录从第i个元素(包括第i个元素)之前的连续子数组的最大和
        dp[0] = nums[0];
        int result = dp[0];//记录最终结果
        for(int i = 1; i < nums.size(); i ++){
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);//因为求连续子数组的最大和,所以要么是取前面最大和加上自己,要么就是以自己为起点重新开始累计和
            if(dp[i] > result) result = dp[i];
        }
        return result;
    }

时间复杂度:O(n)

空间复杂度:O(n)

LeetCode392.判断子序列

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

思路:这道题其实读完题后思路也就出来了。使用双指针法最简单,当然这里还是介绍一下动态规划的思路。

1. 双指针法

使用一个index1指向s,index2指向t,当相等时,index1、index2向后同时移动,当不相等时,只有index2向后移动。最后跳出循环,如果index1指向了s的最后,那么说明s是t的一个子序列,直接返回true,不然返回false。

    bool isSubsequence(string s, string t) {
        int index1 = 0;//标记s中的元素下标
        int index2 = 0;//标记t中的元素下标
        while(index1 < s.size() && index2 < t.size()){
            if(t[index2] == s[index1]){//遇到相等的,那么s、t中的下标各往后加1
                index2 ++;
                index1 ++;
            }else{
                index2 ++;//如果不相等的话,那就只是t的下标向后加1
            }
        }
        if(index1 == s.size()) return true;//如果循环跳出的时候,index1指向了s的最后,那么说明s是t中的子序列
        return false;
    }

时间复杂度:O(n)

空间复杂度:O(1)

2. 动态规划

这里采用动态规划,还是和上面的求最长公共子序列一样的思路,dp[i][j]表示dp[i][j]表示以下标i-1结尾的字符串s与以下标j-1结束的t字符串的最大公共子序列长度,最后如果dp[s.size()][t.size()]==s.size(),那么直接返回true。

不同点就在于在循环中元素不相等时所记录的状态,为什么要记录前面的状态,是因为题目不要求连续,所以需要记录前面的最大相同子数组长度。当不相等时,这里的dp[i][j]=dp[i][j-1],没有了与dp[i-1][j]比较然后取最大值,为什么呢?

因为这里的i下标对应的是s字符串,相当于它是一个定量,而j对应的t字符串是一个变化的量,当元素不相等的时候,就要删除掉这个不相等的元素,因为它不符合题意。但是我要在这里将状态记下,后续的时候要使用的,所以使用的是dp[i][j-1]。

s数组是不能删除的,因为本身就是在t数组里找s数组,相当于只有t数组能修改,涉及到s数组的,前面的状态已经有了,不可能说已经识别了三个元素相等了,再去在这个地方标记为两个元素相等,完全是不合理的,识别了三个就应该记录三个,变的应该是t数组,而不应该是s数组中的元素。

如果说这一点理解清楚了,那么这道题也就简单了。

    bool isSubsequence(string s, string t) {
        //dp[i][j]表示以下标i-1结尾的字符串s与以下标j-1结束的t字符串的最大公共子序列长度
        vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
        for(int i = 1; i < s.size() + 1; i ++){
            for(int j = 1; j < t.size() + 1; j ++){
                if(s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = dp[i][j - 1];//这里说明s[i-1]与t[j-1]不相同,那么就相当于t[j-1]这个元素需要排除,那么此时dp[i][j]的状态就等于了dp[i][j-1]的状态,相当于继承了以下标i-1结尾的字符串s与以下标j-2结束的t字符串的最大公共子序列长度的状态,
                //这里和之前的最大公共子序列在元素不相同时的更新方法的区别就在于对于s数组是不需要找到它下标i-2的dp数组状态,因为这里的s相当于一个固定值,要操作的是t字符串,看它是否还有s这样一个子序列,所以也就是说,不需要对s进行删除、排除之类的操作,而是需要对t字符串中的元素进行排除、删除
            }
        }
        if(dp[s.size()][t.size()] == s.size()) return true;//最长公共子序列的长度为s的长度即可返回true,说明s是t的子序列
        return false;
    }

时间复杂度:O(n*m)

空间复杂度:O(n*m)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值