力扣● 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];
    }
};
相关推荐
m沐沐6 小时前
【深度学习】YOLOv2目标检测算法——改进点、网络结构与聚类先验框解析
人工智能·pytorch·深度学习·算法·yolo·目标检测·transformer
不会就选b6 小时前
算法日常・每日刷题--<链表>3
数据结构·算法·链表
测试界的世清6 小时前
各大厂软件测试面试题+答案纯干货
面试·职场和发展
geovindu7 小时前
go: Iterative Algorithms
开发语言·后端·算法·golang·迭代算法
SunShaoLu8 小时前
实测7款思维导图工具图片导出能力:PNG/SVG/PDF全支持,附选型指南
职场和发展·效率工具·思维导图
Zachery Pole9 小时前
CCF-CSP备战NO.7【队列】
算法
闪电悠米9 小时前
力扣hot100-48.旋转图像-转置翻转详解
算法·leetcode·职场和发展
满天星83035779 小时前
【算法】最长递增子序列(三种解法)
算法
啦啦啦啦啦zzzz10 小时前
算法:贪心算法
c++·算法·leetcode·贪心算法
清泓y12 小时前
RAG 技术
算法·ai