进阶理解:leetcode115.不同的子序列(细节深度)

news2025/1/17 6:08:33

这道题是困难题,本章是针对于动态规划解决,对于思路进行一个全面透彻的讲解,但是并不是对于基础讲解思路,而是渗透到递推式和dp填数的详解,如果有读者不清楚基本的解题思路,请看我的这篇文章算法训练营DAY53|392.判断子序列、115.不同的子序列-CSDN博客

但在看的过程中,你可能会有为什么把dp数组设为以i-1和j-1的下标这样的奇怪含义的困惑,这是为了方便初始化,不用对每一个位置进行一开始的判定初始化,也就是说可能要对于一开始下标匹配做一些多余的处理,详细的解释可以翻看我往期作品,其中会有更为详细的介绍,本期文章,我主要是针对,对于思路有一些了解,但是对于题解本身仍存有一些疑惑的读者准备的。

当然也是对于 我之前写的那期题解未能写的清楚的一些部分的补充。


这里还要纠正一下第一次写那个题解的一部分话是存在一些问题的,两个字符匹配的第一项并不是未匹配成功的结果,如果读者不能明白我在说什么直接看下面的代码即可,不必要去看那期文章,以避免误导读者。

class Solution {
public:
    int numDistinct(string s, string t) {
        int m=s.size(),n=t.size();
        vector<vector<uint64_t>>dp(m+1,vector<uint64_t>(n+1,0));
        for(int i=0;i<=m;++i)dp[i][0]=1;

        for(int i=1;i<=m;++i){
            for(int j=1;j<=n;++j){
                if(s[i-1]==t[j-1])dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
                else dp[i][j]=dp[i-1][j];
            }
        }
        return dp[m][n];
    }
};

如上是题目正确的代码,可以ac。

首先我们来分析的是递推式,然后是初始化和打表,最后是整体的总结。

先粘上一张代码随想录的图片,当大家需要看图对比时候可以回过头开看:

请注意:子序列问题中dp解决时,通常在需要在两个数组或字符串中找答案时候,大多数情况下都需要开辟二维dp数组来解决问题。
明确递推式dp【i】【j】代表以i-1和j-1结尾的字符串s和t,当前情况下,s字符串中t出现的个数有多少个。
递推公式:当i-1和j-1位置上的s和t字符串对应下标字符相等,则当前位置可由dp【i-1】【j-1】推出来,这个递推式的意思是由于这两个字符相等所以不用管该两个字符,
然后去看相当于i-2和j-2对应的位置下,s中包含了多少个t。
你也可以理解为当前的dp【i】【j】要填数,就要看上一个状态时,也就是dp【i-1】【j-1】的状态,因为此时对应下标的元素相等,所以直接去看上一个状态,而此时不需要删除元素
那上一个状态dp【i-1】【j-1】有没有可能需要删除元素呢?当然有可能,但这并不是我们本次填数需要解决的问题,上一个状态肯定在上一个状态被解决了。
为什么不是dp【i-1】【j-1】+1?当前字符相等应该意味着我们又多匹配成功了一个字符,所以理应+1啊
看这两个【bag】和【bag】当该填数dp【2】【2】时候,我们根据dp数组定义实际上看的是s的i-1下标,和t的j-1下标位置
那么也就是此时正在比较【ba】和【ba】呢,它的上一个状态是【b】【b】虽然匹配是相等,但是这并不意味着我们只要匹配相等字符就得+1
因为这道题求的不是重复字符最多有多少个!!!
那怎么完成匹配成功t字符串时候加入答案呢?因为有第二项递推式!
是dp【i-1】【j】这个递推式就相当于不回退当前的t字符串的匹配下标,而是回退s字符串下标,看看此时状态是否有s中有t字符串的这种情况
举例:【bagg】和【bag】这两个,那么除了s字符中的s[0]s[1]s[3]可以得到t字符串,s[0]s[1]s[2]这个组合也可以得到t字符串。
所以这正是我们可以回退s的原因,换个角度去看,这也是我们匹配t这个字符串的整个字符串的方法。把这两个递推式相加就可以得到dp【i】【j】了
我们不考虑t字符串回退的原因,在这里是因为我们是在s字符串里找有多少个t,而不是在t里找,所以应该回退s里的字符。如果你选择回退t此时对应的下标,那么将不可能得到正确答案,因为无论最后s能不能
从中找到t字符串的完整序列,只要t中的部分子数组在s中出现过,那么它一直沿用前面的状态,那是不是肯定会得到1次匹配?
你想想t一直沿用前面的状态,而最开始的状态是s有字符而t无字符,所以一定会有一解。这得出的答案一定是错误的。
所以t不可回退字符,t只能向前匹配,这样保证了求到最后的是s里是否包含完整的t字符串,而不是只要包含一部分子数组就可以完成匹配。
还有一件事是:s字符下标每次增大1然后重新对t字符串进行匹配,也就解决了以各个的s下标位置为截止,能匹配到最大匹配个数是多少。将它们加起来就是答案。


而如果此时两字符对应不等怎么办
dp【i】【j】=dp【i-1】【j】这里的递推式你可以理解为,删除s字符串当前遍历的字符(模拟删除)而转为用该下标的前一个位置看看有没有
s中含有t的时候,延续那时候的状态就可以了。


初始化也很重要它是得到答案的关键初始化dp数组,s和t数组都为空时候对应dp应该为1,解释:两字符串都为空,则s字符串里包含1个t
当s字符串有内容,t为空,那么s各个状态都为1,解释:s字符串删除全部字符得到空字符串也就是t
当s没内容,t有内容,则这些状态都为0,解释:s无论如何也无法推出t
当两字符串内容不相等时候,也就是s中不包含t那么无论如何也推不出一个,因为s里要想能找到t,那么必须t中的第一个元素与s中的某位置元素发生了起始匹配。
这时初始化产生作用,虽然这种中间某处得到的匹配,匹配不上原始的dp【0】【0】但是它能匹配上dp【i】【0】使得累计1次匹配。
然后接下来的匹配,就是对t字符串各个字符都必须连续匹配,才可以使这个1继承下来,因为递推式的缘故,往后匹配,dp【0】【0】肯定匹配不上
而如果中间某字符没匹配上断档了,那么再匹配上的时候,dp【i-1】【j】也不可能借用到dp【i】【0】就使得最终答案是0。


打表特殊测试用例  s=【bag】t=【bag】
dp[0][0]=1dp[0][1]=0dp[0][2]=0dp[0][3]=0
dp[1][0]=1dp[1][1]=1dp[1][2]=0dp[1][3]=0
dp[2][0]=1dp[2][1]=1dp[2][2]=1dp[2][3]=0
dp[3][0]=1dp[3][1]=1dp[3][2]=1dp[3][3]=1
可见即使中间遍历时候也有完全匹配时候【ba】【ba】但是这并不会使dp【2】【2】变成2
由头上的图片可以知道其实dp【i-1】【j-1】和dp【i-1】【j】都有可能是答案的组成部分
打表bac和bad
dp[0][0]=1dp[0][1]=0dp[0][2]=0dp[0][3]=0
dp[1][0]=1dp[1][1]=1dp[1][2]=0dp[1][3]=0
dp[2][0]=1dp[2][1]=1dp[2][2]=1dp[2][3]=0
dp[3][0]=1dp[3][1]=1dp[3][2]=1dp[3][3]=0

再写该题时候,写到循环部分的时候,想的是t字符串既然不能使之回退,那么是不是应该写成t循环在外面,s循环在里面,这样只循环了一次t,就保证了不回退的效果,而不是写成s循环在外面t循环在内部
但后来发现不是这么一回事,这样写循环是有原因的,它遍历s的各个字符给t去进行匹配,以获得所有的组合答案。如果t循环写外面了,t确实没有回退,但是这样写的思路不就成了拿t的各个元素去找
t里面是否存在s字符串了吗?这不就写反了吗?
还有就是外层for是s内层for是t,并不是没有实现t的禁止回退,所谓t字符串不能回退不是指t的字符串不能重复遍历,不能回退体现在递推公式上,不要弄混淆。
再细说一下具体的匹配机理,是如何实现s拿一个个字符串然后去匹配t的。
首先s字符串每次增大一个匹配范围(表面上看),然后t字符串在s每次增大匹配范围时,重新遍历t字符串去和s的现在范围去匹配,看看现在的s范围能否找到更多的匹配次数并填到dp数组内。
从代码的实际运行角度,s的每次增大一个字符的范围,t重新匹配,并不是匹配0——i,而是拿t的整个字符串去和i这个元素单单去匹配,而非s子串和t字符串匹配。
这实际上无形之中提升了效率,那么怎么实现匹配的正确呢?
递推式已经给出了,如果当前元素匹配不上,那么当前位置填数继承上一次匹配,也就是当前两个字符不匹配,那么相当于放弃当前两个字符的匹配机会,而是选用上一次的对应截止字符处,去继承那一次的
匹配成果。这次的继承是就是回退s而不回退t
如果匹配成功,有两个方向推出,一个是不考虑当前两个字符,因为两个字符当前匹配成功,所以暂时不考虑,但是还没完,还要考虑s往前回退一个字符的情况与t进行匹配的成果,将二者相加得到答案
为什么不回退第二个字符串t我们上面已经说得很清楚了,而回退s为上一个字符是为什么呢?就是因为bag和bagg的测试用例这类情况,也即是说可能存在上一个s的字符也存在匹配的关系,那有人要问
如果是bag和baggg呢?你怎么匹配?这是一样的,第二次的g会继承第一次g匹配成果,而第三次又会继承第二次.看递推式就能知道,两字符匹配成功,答案是左上角数加上头上的数,
而这个头上的数,自然把上一层的g加过来了。
还有一个问题是为什么总是能得到正确答案,我们上面已经说了一些了,但是个人觉得还不够透彻。再说一次
首先它为什么不是每匹配成功一次都+1我们上面说的很清了,当两个字符相等时候的填数再说一下
我们由于初始化的缘故,使得若此时匹配的两个字符匹配不相等,也就是说假如bagg和bag,s字符串bagg匹配到最后一个字符时候,但是t字符串从头开始匹配,这个时候我们由于第二递推式,匹配不等时候会拿
dp【i】【j-1】的数做继承,直到匹配成功填数的时候,真正的递推作用才开始显现
匹配相等时,会把头上的位置也就是dp【i-1】【j】和dp【i-1】【j-1】累加起来,换句话说只有两个字符最后发生匹配时候,才能够获得答案,如果不发生匹配不会获得答案。
这是为什么呢?不是说不匹配会继承前位置吗?但是你要注意,只有同列才能继承,也就是说如果你的上一行列没有做事,那么你不能享受成果,这就相当于祖上得有财富积累,你才能躺平拿到成果,否则你只能
依赖于本身的才能,也就是进入第一递推式,虽然祖上可能没有成果,但你可以收获你左上角人的成果。
所以并不会发生一处匹配成功,全部人都受益的结果。
你左上的成果相当于叔辈的成果,你没有能力时候它不会贡献成果给你享用,只有你的亲祖先才能共享成果,所以如果前面出现多次匹配,也就是说祖先和叔辈都得到了匹配,或他们继承它们的祖先和叔辈匹配
而且同时你也有能力匹配,才会同时得到两方面的匹配成果。
这个左上角匹配也并非与你完全无关,它有数字代表了发生连续匹配。
badgbacg打表
dp[0][0]=1dp[0][1]=0dp[0][2]=0dp[0][3]=0dp[0][4]=0
dp[1][0]=1dp[1][1]=1dp[1][2]=0dp[1][3]=0dp[1][4]=0
dp[2][0]=1dp[2][1]=1dp[2][2]=1dp[2][3]=0dp[2][4]=0
dp[3][0]=1dp[3][1]=1dp[3][2]=1dp[3][3]=0dp[3][4]=0
dp[4][0]=1dp[4][1]=1dp[4][2]=1dp[4][3]=0dp[4][4]=0
这下应该明白了吧,虽然祖上有成果的某些中间量会继承祖上的成果,但这并不一定影响最后的答案,该样例中即使最后一次发生了g和g的匹配,但是它的左上角没有成果也就是出现了断档,而且它的祖上也没有
成果,所以它依然无法收获结果。
这也就是说只要中间断档发生了一次,那么就不再可能延续的上,想尝试的读者可写一个badgh和bacgh试一下打表
简而言之:
对于当前两字符匹配成功,则需要收获结果,结果的第一部分代表了之前字符的连续匹配,第二部分代表了s中存在多个t的继承匹配个数
对于匹配不成功,继承连续匹配结果,这一举动主要是为了下一次的s扩大后的匹配受益,因为s当前字符不可能与t所有字符匹配,那么只要有一次完成了匹配,则这一列的所有位置理应继承那次匹配成功的成果。
以应对下一次循环匹配的时候,对于连续字符匹配的连贯性。
 


相信如果读者事先具备了代码思路,知道题解如何解答,再看这篇文章一定会受益匪浅。

这是对于细节上问题的一些补充。本章就到这里。

都看到这里了如果对您有用的话别忘了一键三连哦,如果是互粉回访我也会做的!

大家有什么想看的题解,或者想看的算法专栏、数据结构专栏,可以去看看往期的文章,有想看的新题目或者专栏也可以评论区写出来,讨论一番,本账号将持续更新。
期待您的关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1225163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lstm+transformer的刀具磨损预测

视频讲解: 基于Lstm+transformer的刀具磨损预测实战_哔哩哔哩_bilibili 结果展示: 数据展示: 主要代码: # pip install openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple/ # pip install optuna -i https://pypi.tuna.tsinghua.edu.cn/simple/ import numpy as np…

CI/CD -gitlab

目录 一、常用命令 二、部署 一、常用命令 官网&#xff1a;https://about.gitlab.com/install/ gitlab-ctl start # 启动所有 gitlab 组件 gitlab-ctl stop # 停止所有 gitlab 组件 gitlab-ctl restart # 重启所有 gitlab 组件 gitlab-ctl statu…

基于token的鉴权机制-JWT

在实际开发项目中&#xff0c;由于Http是一种无状态的协议&#xff0c;我们想要记录用户的登录状态&#xff0c;或者为用户创建身份认证的凭证&#xff0c;可以使用Session认证机制或者JWT认证机制。 什么是JWT? 网络应用环境间传递声明执行的一种基于JSON的开放标准。适用于…

设计模式-外观模式-笔记

“接口隔离”模式 在组件构建过程中&#xff0c;某些接口之间的依赖常常带来很多问题、甚至根本无法实现。采用添加一层间接&#xff08;稳定&#xff09;接口&#xff0c;来隔离本来互相紧密相关的接口是一种常见的解决方案。 经典模式&#xff1a;Facade、Proxy、Adapter、…

【APUE】补充 — 基于管道的线程池

目录 一、引言 二、代码实现 三、思考 一、引言 在线程章节的 3.2 部分&#xff0c;我们曾经提到过线程池的实现 在当时的代码中&#xff0c;我们仅仅用的一个 int 类型的变量来表示这个“池”&#xff0c;用来存放任务 显然这个池太小了&#xff0c;如果下游线程很多&am…

强化学习在文生图中的应用:Training Diffusion Models with Reinforcement Learning

论文链接:Training Diffusion Models with Reinforcement Learning项目地址:Training Diffusion Models with Reinforcement Learning官方代码:https://github.com/kvablack/ddpo-pytorch/tree/maintrl实现:https://huggingface.co/docs/trl/ddpo_trainer🤗关注公众号 fu…

【LeetCode刷题-双指针】--16.最接近的三数之和

16.最接近的三数之和 方法&#xff1a;排序双指针 class Solution {public int threeSumClosest(int[] nums, int target) {Arrays.sort(nums);int ans nums[0] nums[1] nums[2];for(int i 0;i<nums.length;i){int start i1,end nums.length - 1;while(start < en…

复合、委托、继承

1. 单例模式 静态实例对象在getInstance函数中定义&#xff0c;这样只有在调用函数时才会生成对象 2. 复合 1. 类中封装另一个类某些功能&#xff1b; 2. 构造、析构的调用过程 指明了复合中如何调用被包含类的构造函数&#xff0c;可以直接写在初始化列表位置&#xff1b; 3.…

Java的IO流-缓冲流

字节缓冲流 package com.itheima.d2;import java.io.*;public class Test1 {public static void main(String[] args) {try (InputStream is new FileInputStream("IO/src/itheima01.txt");//1、定义一个字节缓冲输入流包装原始的字节输入流InputStream bis new Bu…

企业是否需要单独一套设备管理系统?

在现代企业中&#xff0c;设备管理是一个至关重要的环节。随着科技的不断进步和信息化的发展&#xff0c;企业对设备管理的要求也越来越高。为了提高设备管理的效率和准确性&#xff0c;许多企业开始考虑是否需要单独一套设备管理系统。本文将从设备管理系统的介绍、和其他系统…

融合语言模型中的拓扑上下文和逻辑规则实现知识图谱补全11.18

融合语言模型中的拓扑上下文和逻辑规则实现知识图谱补全 摘要1 引言2 相关工作2.1 事实嵌入法2.2 拓扑嵌入方法2.3 规则融合方法2.4 基于LM的方法 3 准备3.1 知识图谱和拓扑上下文3.2 KG中的逻辑规则4.3 三元组嵌入 5 实验和结果5.1 数据集和评价指标 摘要 知识图补全&#xf…

电子学会C/C++编程等级考试2021年06月(一级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:数的输入和输出 输入一个整数和双精度浮点数,先将浮点数保留2位小数输出,然后输出整数。 时间限制:1000 内存限制:65536输入 一行两个数,分别为整数N(不超过整型范围),双精度浮点数F,以一个空格分开。输出 一行两个数,分…

c语言:模拟实现qsort函数

qsort函数的功能&#xff1a; qsort相较于冒泡排序法&#xff0c;不仅效率更快&#xff0c;而且能够比较不同类型的元素&#xff0c;如&#xff1a;浮点数&#xff0c;结构体等等。这里我们来模拟下qsort是如何实现这一功能的&#xff0c;方便我们对指针数组有一个更深层次的理…

电子画册真的好好用,制作也简单,都快来学学!

同纸质画册相比&#xff0c;电子画册无需受时间、空间、地域等限制&#xff0c;它通过手机、电脑即可发送文件&#xff0c;轻松实现在线浏览&#xff0c;使用起来更方便。 如何制作电子画册&#xff1f;这里同大家分享一下超简单的电子画册制作教程&#xff0c;0基础也能轻松上…

JDBC,Java连接数据库

下载 JDBC https://mvnrepository.com/ 创建项目&#xff0c;然后创建一个目录并将下载好的 jar 包拷贝进去 选择 Add as Library&#xff0c;让这个目录能被项目识别 连接数据库服务器 在 JDBC 里面&#xff0c;使用 DataSource 类来描述数据库的位置 import com.mysql.cj.…

The ultimate UI kit and design system for Figma 组件库下载

Untitled UI 是世界上最大的 Figma UI 套件和设计系统。可以启动任何项目&#xff0c;为您节省数千小时&#xff0c;并祝您升级为专业设计师。 采用 100% 自动布局 5.0、变量、智能变体和 WCAG 可访问性精心制作。 900全局样式、变量&#xff1a;超级智能的全局颜色、排版和效…

基于机器学习的居民消费影响因子分析预测

项目视频讲解: 基于机器学习的居民消费影响因子分析预测_哔哩哔哩_bilibili 主要工作内容: 完整代码: import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import missingno as msno import warnings warnings.filterwarnin…

1.rk3588的yolov5运行:pt_onnx_rknn转换及rknn在rk3588系统python运行

自己有点笨&#xff0c;查资料查了一周才完美的实现了yolov5在rk3588环境下的运行&#xff0c;在这里写具体步骤希望大家少走弯路。具体步骤如下&#xff1a; 一、yolov5的原代码下载及pt文件转换为onnx文件 1.yolov5的原代码下载及环境搭建 在这里一定要下载正确版本的源代码…

vue引入前端工程内的图片

一、public目录下的图片 public目录下的图片引入方式&#xff1a; <!--/images/图片名称&#xff0c;这种属于绝对路径&#xff0c;/指向public目录 --> <img src"/images/image.png"> 二、src目录下的图片 先在vue.config.js进行配置&#xff0c;并指…

Flutter笔记:Matrix4矩阵变换与案例

Flutter笔记 Matrix4矩阵变换及其案例 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/134474764 【简介…