力扣● 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];
    }
};
相关推荐
其实防守也摸鱼1 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
Nuanyt2 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
w_zero_one5 小时前
递归算法(一)
java·开发语言·算法
圣保罗的大教堂5 小时前
leetcode 3736. 最小操作次数使数组元素相等 III 简单
leetcode
文丑颜不良啊6 小时前
LeetCode1881~1990题目解析
java·leetcode
风影同志783246 小时前
20 个比特能装下多少行情?——实测拆解 Kronos 的 K 线分词器
算法
写的都是BUG7 小时前
APK 里的钥匙,一直就放在门口:聊聊加固真正该做什么
前端·算法
lsc_blog7 小时前
年底还在招的岗位,先确认缺口和到岗要求
git·缓存·ci/cd·职场和发展·github
地平线开发者7 小时前
CLIP 的零样本分类:一组文本如何变成图像分类器?
算法