● 392.判断子序列
可以直接使用双指针的方法,2个指针分别从s、t开头出发,时间复杂度为O(t.size())。
但是这里用动规来做。Carl:掌握本题的动态规划解法是对后面要讲解的编辑距离的题目打下基础。
so绕一下,用昨天的● 1143.最长公共子序列来写,dp[n1][n2]就是两个数组的最长公共子序列的长度,如果等于s.size()就是true。注意 ● 1143.最长公共子序列 是不以A[i-1]/B[j-1]为结尾的。因为是要求不连续子序列,所以不需要知道序列最后一个元素的值。
但是通过代码随想录,发现递推公式那还有可以改进的地方。
我们知道● 1143题,如果A[i-1]等于B[j-1]的话,dp[i][j]直接就是dp[i-1][j-1]+1。如果不相等的话, A[i-1]可能和B[j-1]之前的相等,B[j-1]可能和A[i-1]之前的相等,所以要取这两种情况的最大值:
cpp
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
学了昨天的不相交的线,可以知道这两种情况分别对应偏向左下的线和偏向右下的线,因为
● 1143.最长公共子序列 ● 1035.不相交的线 都没有说明2个字符串的长度谁大谁小,所以2种情况都有可能,但是这道题s比t短,只可能s[i-1]和t[j-1]之前的相等。所以画这一条线的话,只可能是垂直(dp[i-1][j-1]+1),或者偏向一个方向(dp[i][j]=dp[i][j-1])。
所以递推公式修改为:
cpp
if(s[i-1]==t[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
else dp[i][j]=dp[i][j-1];
代码:
cpp
class Solution {
public:
bool isSubsequence(string s, string t) {
int n1=s.size();
int n2=t.size();
vector<vector<int>> dp(n1+1,vector<int>(n2+1,0));
for(int i=1;i<=n1;++i){
for(int j=1;j<=n2;++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];
}
}
return (dp[n1][n2]==s.size());
}
};
对比这三道题:● 1143.最长公共子序列; ● 1035.不相交的线;● 392.判断子序列
打印:
● 115.不同的子序列
困难题,有点难理解。主要是dp数组的含义。
1.dp数组含义。
dp[i][j]:在s数组[0,i-1](可不以s[i-1]结尾)范围中出现t[0,j-1](必须以t[j-1]结尾)的个数为dp[i][j]。
如果强制以s[i-1],因为子序列不用连续,所以又得查看i-1以及之前的dp元素,不能体现动规的思想还可能超时。
2.递推公式。
因为不一定以s[i-1]结尾,所以相等不相等2种情况都要考虑:
(1)如果s[i-1]==t[j-1]。
根据dp数组定义,可以不以s[i-1]结尾。那么子序列又有2种情况:①以s[i-1]结尾。dp[i][j]代表的序列包含了s[i-1]和t[j-1],所以dp[i][j]应该继承了dp[i-1][j-1];②不以s[i-1]结尾,即不考虑s[i-1],那么t[j-1]出现在s[i-1]之前,根据dp定义,就应该从不包含s[i-1]的子数组[0,i-1]中出现t[0,j-1]的个数,得到dp[i][j],也就是dp[i-1][j]。
举例:bagag,bag。计算dp[4][2]的时候,s[4]=t[2],所以以s[4]结尾的话,可以在之前的序列ba里面加上s[4]:++ba++ ga++g++,++bag++ 。也可以不以s[4]结尾,那么不考虑s[4],从s的[0,3]里面找t[0,2]的个数,即选择s[4]之前的s[2]:++ba++ ga++g++,++bag++ 。所以这样就考虑了s数组里面可能会出现>=2个t[j-1]的情况。
分类加法分步乘法,这2种情况是分类的,会同时出现,且求的就是所有情况个数,所以相加。
即:dp[i][j]=dp[i-1][j]+dp[i-1][j-1]。
(2)如果不等。
不相等的话,子序列肯定不会以s[i-1]结尾,所以直接不考虑s[i-1],继承不包含s[i-1]的连续子数组[0,i-1]中出现t[0,j-1]的个数即可。
即:dp[i][j]=dp[i-1][j]。
3.初始化。
dp定义需贯穿始终。根据定义,i和j都只能从1开始,所以dp[0][j]和dp[i][0]都需要初始化。
dp[0][j]:s数组[0,0]里面包含t[0,j-1]的个数,如果j=0,没有意义。先看j>0,t非空,所以dp[0][j]肯定全为0。
dp[i][0]:s数组[0,i]里面包含t[0,-1]的个数,t[0,0]是t[0]这个元素,所以t[0,-1]代表的应该是空串。空串是否是字符串的子串?回忆子序列的定义:字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。所以空串就是将s所有元素都删除的子序列。想起数据结构说过空串是字符串的一个子串。
所以dp[i][0]应该初始化为1。
dp[0][0]也应该是1,空串也是空串的子序列。
4.遍历顺序。
根据递推公式,从上到下,从左到右递推。
5.打印。
代码:
cpp
class Solution {
public:
int numDistinct(string s, string t) {
int n1=s.size();
int n2=t.size();
//数据类型使用uint64_t,比long long更大,不会溢出。
vector<vector<uint64_t>> dp(n1+1,vector<uint64_t>(n2+1,0));
for(int i=0;i<=n1;++i)dp[i][0]=1;
for(int i=1;i<=n1;++i){
for(int j=1;j<=n2;++j){
if(s[i-1]==t[j-1])dp[i][j]=dp[i-1][j]+dp[i-1][j-1];
else dp[i][j]=dp[i-1][j];
}
}
return dp[n1][n2];
}
};