力扣● 392.判断子序列 ● 115.不同的子序列

● 392.判断子序列

可以直接使用双指针的方法,2个指针分别从s、t开头出发,时间复杂度为O(t.size())。

但是这里用动规来做。Carl:掌握本题的动态规划解法是对后面要讲解的编辑距离的题目打下基础

so绕一下,用昨天的● 1143.最长公共子序列来写,dpn1n2就是两个数组的最长公共子序列的长度,如果等于s.size()就是true。注意 ● 1143.最长公共子序列 是不以Ai-1/Bj-1为结尾的。因为是要求不连续子序列,所以不需要知道序列最后一个元素的值。

但是通过代码随想录,发现递推公式那还有可以改进的地方。

我们知道● 1143题,如果Ai-1等于Bj-1的话,dpij直接就是dpi-1j-1+1。如果不相等的话, Ai-1可能和Bj-1之前的相等,Bj-1可能和Ai-1之前的相等,所以要取这两种情况的最大值:

cpp 复制代码
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

学了昨天的不相交的线,可以知道这两种情况分别对应偏向左下的线和偏向右下的线,因为

● 1143.最长公共子序列 ● 1035.不相交的线 都没有说明2个字符串的长度谁大谁小,所以2种情况都有可能,但是这道题s比t短,只可能si-1和tj-1之前的相等。所以画这一条线的话,只可能是垂直(dpi-1j-1+1),或者偏向一个方向(dpij=dpij-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数组含义。

dpij:在s数组0,i-1(可不以si-1结尾)范围中出现t0,j-1(必须以tj-1结尾)的个数为dpij

如果强制以si-1,因为子序列不用连续,所以又得查看i-1以及之前的dp元素,不能体现动规的思想还可能超时。

2.递推公式。

因为不一定以si-1结尾,所以相等不相等2种情况都要考虑:

(1)如果si-1==tj-1

根据dp数组定义,可以不以si-1结尾。那么子序列又有2种情况:①以si-1结尾。dpij代表的序列包含了si-1和tj-1,所以dpij应该继承了dpi-1j-1;②不以si-1结尾,即不考虑si-1,那么tj-1出现在si-1之前,根据dp定义,就应该从不包含si-1的子数组0,i-1中出现t0,j-1的个数,得到dpij,也就是dpi-1j

举例:bagag,bag。计算dp42的时候,s4=t2,所以以s4结尾的话,可以在之前的序列ba里面加上s4:++ba++ ga++g++,++bag++ 。也可以不以s4结尾,那么不考虑s4,从s的0,3里面找t0,2的个数,即选择s4之前的s2:++ba++ ga++g++,++bag++ 。所以这样就考虑了s数组里面可能会出现>=2个tj-1的情况。

分类加法分步乘法,这2种情况是分类的,会同时出现,且求的就是所有情况个数,所以相加。

即:dpij=dpi-1j+dpi-1j-1

(2)如果不等。

不相等的话,子序列肯定不会以si-1结尾,所以直接不考虑si-1,继承不包含si-1的连续子数组0,i-1中出现t0,j-1的个数即可。

即:dpij=dpi-1j

3.初始化。

dp定义需贯穿始终。根据定义,i和j都只能从1开始,所以dp0j和dpi0都需要初始化。

dp0j:s数组0,0里面包含t0,j-1的个数,如果j=0,没有意义。先看j>0,t非空,所以dp0j肯定全为0。

dpi0:s数组0,i里面包含t0,-1的个数,t0,0是t0这个元素,所以t0,-1代表的应该是空串。空串是否是字符串的子串?回忆子序列的定义:字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。所以空串就是将s所有元素都删除的子序列。想起数据结构说过空串是字符串的一个子串。

所以dpi0应该初始化为1。

dp00也应该是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];
    }
};
相关推荐
指掀涛澜天下惊几秒前
强化学习进阶篇八 策略梯度算法
深度学习·学习·算法·强化学习
乌萨奇也要立志学C++5 分钟前
【洛谷】kmp算法
开发语言·算法
禹凕14 分钟前
Dijkstra算法详解与应用
python·算法
YaraMemo1 小时前
元启发式算法框架
人工智能·算法·5g·信息与通信·启发式算法·信号处理
weixin_307779131 小时前
一维无粘 Burgers 方程的激波形成问题:MacCormack 格式求解
c++·算法·matlab
HugoStudio_SWAN2 小时前
洛谷 B4500 / B4449 / B3843 凯撒密码、密码强度与密码合规——加密与安全的三道门
c++·学习·程序人生·算法·安全
枫叶林FYL3 小时前
【群体智能集群控制工程实践】第3章 一致性协同算法
算法
禹凕3 小时前
滑动窗口算法实战指南
python·算法
DisonTangor3 小时前
【腾讯混元雪耻归来】 Hy4 preview:770B 参数 MoE 旗舰模型,1M 上下文全面开源
人工智能·算法·开源·aigc·腾讯云·腾讯云ai代码助手
明月_清风4 小时前
从二叉树到 B+ 树:一文搞懂工程中「树」的演化之道
数据结构·算法·go