本博客我们梳理用动态规划方法解决子序列问题。
一、最长公共子序列
题目为1143. 最长公共子序列 - 力扣(LeetCode),给定两个字符串 text1
和 text2
,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0
。
使用动规五部曲分析:
①确定dp数组和下标的含义。 本题dp[i][j]我将其设置为text1中[0,i]的字符串和text2中[0,j]的字符串的最长公共子序列。
②递推公式。主要分成两大类,text1[i]和text2[j]相同和不同两类。如果相同,则该元素为相同公共元素,此时dp数组的值应该长度增加1,那么我们在哪个基础上加一呢?我们应该在[0,i-1]和[0,j-1]的比较结果下加一,这样不会重复计算i和j位置上的元素,所以为dp[i][j] = dp[i-1][j-1]+1。如果i-1<0或者j-1<0,则将dp[i][j]设为1即可。如果不相同,说明该位置不能匹配,最长公共子序列保留之前的值,为dp[i-1][j]和dp[i][j-1]之间的较大值。
③初始化。全部初始化为0。
④遍历顺序。由递推公式得遍历顺序为从左到右从上到下。
⑤打印检查dp数组。
完整代码为:
class Solution {
public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>> dp(text1.size(),vector<int>(text2.size(),0));for(int i = 0; i<text1.size(); i++){for(int j = 0; j<text2.size(); j++){if(text1[i] == text2[j]){if(j>0&&i>0) dp[i][j] = dp[i-1][j-1]+1;else dp[i][j] = 1;}else{if(j>0&&i>0) dp[i][j] = max(dp[i-1][j],dp[i][j-1]);if(j==0&&i>0) dp[i][j] = dp[i-1][j];if(i==0&&j>0) dp[i][j] = dp[i][j-1];}}}return dp[text1.size()-1][text2.size()-1];}
};
二、不相交的线
题目为1035. 不相交的线 - 力扣(LeetCode),虽然要求的是不相交的直线,但其实就是在找最长公共子序列,因为相同子序列顺序不改变,直线就不会相交。
代码与上一题完全相同:
class Solution {
public:int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {vector<vector<int>> dp(nums1.size(),vector<int>(nums2.size(),0));for(int i = 0; i<nums1.size(); i++){for(int j = 0; j<nums2.size(); j++){if(nums1[i] == nums2[j]){if(j>0&&i>0) dp[i][j] = dp[i-1][j-1]+1;else dp[i][j] = 1;}else{if(j>0&&i>0) dp[i][j] = max(dp[i-1][j],dp[i][j-1]);if(j==0&&i>0) dp[i][j] = dp[i-1][j];if(i==0&&j>0) dp[i][j] = dp[i][j-1];}}}return dp[nums1.size()-1][nums2.size()-1];}
};
同样的,用这个思路和方法还可以解决判断子序列问题。
三、最大子序和
题目为53. 最大子数组和 - 力扣(LeetCode),之前贪心算法中解决过这个题,现在我们用动态规划来尝试解决一下。
①dp数组和下标的含义。dp[i]表示包括下标i的数的最大连续子序列和。
②递推公式。dp[i]因为计算了nums[i],所以分为两种情况,在i-1的状态上加上nums[i]和以nums[i]开始计算,二者取较大值。
③初始化。初始化dp[0]为nums[0]。
④遍历顺序。从前往后。
⑤检查dp数组。
完整代码如下:
class Solution {
public:int maxSubArray(vector<int>& nums) {vector<int> dp(nums.size());dp[0] = nums[0];int m = nums[0];for(int i = 1; i<nums.size(); i++){dp[i] = max(dp[i-1]+nums[i],nums[i]);if(dp[i]>m) m = dp[i];}return m;}
};
四、不同的子序列
题目为115. 不同的子序列 - 力扣(LeetCode),给你两个字符串 s
和 t
,统计并返回在 s
的子序列中 t
出现的个数,结果需要对10^9+7取模。
如果本题求的是连续的,那就可以用KMP。我们使用动规五部曲分析这个题:
①dp数组和下标的含义。dp[i][j]表示t串中[0,i]与s中[0,j]的子序列匹配数。
②递推公式。dp[i][j]的值取决于t[i]和s[j]是否相等。如果相等,则分为两种情况,使用这两个字母的匹配,则匹配数为dp[i-1][j-1]。如果不使用这两个字母的匹配,则匹配数为dp[i][j-1]。dp[i][j]为这两个数相加。如果不相等,则匹配数为之前的dp[i][j-1]。这里如果不理解可以自己画一下二维数组,例如对于s=baegg和t=bag的情况下,二维数组为[[1,1,1,1,1],[0,1,1,1,1],[0,0,0,1,2]]。
③初始化。在我写本题的时候,还因为初始化出了点小错。这里我对第一列进行初始化。dp[0][0]的值取决于t[0]和s[0]是否相等,第一列剩余值均为0,因为是长度为一个以上的t问长度为1的s里是否有该子序列。第一列的计算在for循环中就不需要计算了因为值已经给出了。
④遍历顺序。我们根据递推公式知道遍历顺序应该为从左到右、从上往下。
⑤打印dp数组检查。本题自己多练几个二维数组自己找规律可以增强自己的理解。也可以换一种思路进行训练,例如可以尝试把s放在列,t放在行。
完整代码如下:
class Solution {
public:int numDistinct(string s, string t) {vector<vector<uint64_t>> dp(t.size(), vector<uint64_t>(s.size(),0));if(s[0]==t[0]) dp[0][0] = 1;else dp[0][0] = 0;for(int i = 0; i<t.size(); i++){for(int j = 1; j<s.size(); j++){if(t[i] == s[j]){if(i>0) dp[i][j] = dp[i][j-1]+dp[i-1][j-1];if(i==0) dp[i][j] = dp[i][j-1]+1;}else{dp[i][j] = dp[i][j-1];}}}return dp[t.size()-1][s.size()-1];}
};
注意本题是数据需要是无符号64位整数,即uint64_t或者unsigned long long。
说明:本文为作者整理知识点用于复习巩固,参考了代码随想录的讲解,有问题可以联系作者欢迎讨论~