当前位置: 首页 > news >正文

代码随想录算法训练营第60期第四十九天打卡

       大家好,今天我们还是继续我们的动态规划章节,可能有的朋友已经开始厌倦了说为什么动态规划怎么这么多题目,大家可以想想我们前面其实刷过好几种类型的动态规划的经典题目比如说各式各样的背包问题当然包括0-1背包,完全背包,大家也知道这两种背包问题的差异就在于物品是否可以选择多次,后面我们还刷了打家劫舍问题,当然还有比较难的买股票的最佳时期问题,前天我们也开始我们的子序列问题,那我们今天还有一种比较重要的问题就是编辑距离问题,那我们就一起走进我们今天的题目。

第一题对应力扣编号为115的题目不同的子序列

       其实大家看到这道题的时候大家应该会想起我们昨天刷过一道题目叫做判断子序列,其实我可以告诉大家,这道题要比那道题难一些,我们来看看这道题的题目要求:

        看完题目我们明白了,其实跟那道题有点类似的地方就是都涉及到了子序列的问题,但是这道题我们是需要求出s的子序列中t出现的次数,这个其实就有难度了,当然我们也知道本题目应该是可以使用动态规划的思路来解题的,那我们就尝试使用动规五部曲来解决这道题目:

       第一步确定dp数组以及下标的含义,dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。我们前面有道题说过为了初始化方便我们是i-1而不是i。

        第二步确定递推公式,感觉这个就有难度了,其实我们做的题目多了就会有感觉,我们应该还是需要看对应位置上的元素是否相等,第一种情况是s[i - 1] 与 t[j - 1]相等,第二种情况是s[i - 1] 与 t[j - 1] 不相等,当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]。还有一种情况就是一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。另一种情况就是s[i - 1] 与 t[j - 1]不相等时,我们这时候其实相当于dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]。

       第三步是dp数组的初始化,从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,那么 dp[i][0] 和dp[0][j]是一定要初始化的。那其实也很明显,我们的dp[i][0]应该初始化为1,而且我们的dp[0][j]应该初始化为0,但是大家要注意我们dp[0][0]应该是初始化为1,因此我们dp[0][j]的初始化应该是从1开始初始化为0.

       第四步就是遍历顺序的确定,其实很明显就是从前往后,这个就无需多说了,其实严格来说应该是左上方和正上方推出来的。

      根据以上分析我们可以给出代码:

class Solution {
public:int numDistinct(string s, string t) {//初始化dp数组vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));//dp数组初始化for (int i = 0; i <= s.size(); ++i) dp[i][0] = 1;for (int j = 1; j <= t.size(); ++j) dp[0][j] = 0;for (int i = 1; i <= s.size(); ++i){for (int j = 1; j <= t.size(); ++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[s.size()][t.size()];}
};

       其实本题的难点就在于理解状态转移方程,我们本题的dp数组的含义其实还是比较抽象的,我们要比较对应位置的元素是否相等,如果相等的话我们应该如何操作,如果不相等的话我们应该如何操作。其实我一想了好久才大致明白我们的状态转移,我开始也思考了为什么我们不考虑用不用t[j-1]后来发现是因为我没有仔细审题,题目问的是s中有多少个t,所以重点考虑的应该是s[i-1],还有如果我们对应的元素不相等,其实我们相当于模拟删除s里面的s[i-1]因此我们这里应该是dp[i-1][j],相等的话我们则不需要考虑两个字符串的最后一个元素,但是不相等的话我们的确是需要考虑。这点大家要多思考。

第二题对应力扣编号为583的题目两个字符串的删除操作

        我们来到今天的第二题,第一题真给我差点整不会了,大家务必要仔细思考一下,我们来看今天的第二题是什么意思:

       其实题目理解起来并不算难,就是我们要使得两个字符串相同我们就需要进行增加删除操作,我们需要求我们最少的操作数是多少,我们还是得使用动规五部曲的思路来解决这道题目:

        第一步确定dp数组以及下标的含义,我们应该是dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。这个定义起来其实不难,但是难的应该是递推公式。

        第二步确定递推公式,同样我们还是得考虑对应位置的元素是否相等,也就是当word1[i - 1] 与 word2[j - 1]相同的时候,和当word1[i - 1] 与 word2[j - 1]不相同的时候,先来看第一种情况就是对应位置元素相同的时候,当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1],这个其实好理解,对应位置相同我们这里的元素无需操作只需看前面即可,但是比较难的是第二种情况,这个其实又有好几种情况,情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1,删word2[j - 1],最少操作次数为dp[i][j - 1] + 1,同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2,那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}),其实这里想一次性想得很全面我个人感觉是比较难的。

        第三步dp数组的初始化,从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。但其实似乎又不难理解,dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。dp[0][j]的话同理,不应该是删除j个因此dp[0][j]=j,这样我们的初始化就完成了。

        第四步确定遍历顺序,这个其实也毫无疑问,从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据左上方、正上方、正左方推出来的。

        那通过以上分析我们就可以尝试给出本题的解题代码:

class Solution {
public:int minDistance(string word1, string word2) {//定义dp数组vector<vector<int>> dp(word1.size() + 1,vector<int>(word2.size() + 1));//dp数组的初始化for (int i = 0; i <= word1.size(); ++i) dp[i][0] = i;for (int j = 0; j <= word2.size(); ++j) dp[0][j] = j; for (int i = 1; i <= word1.size(); ++i){for (int j = 1; j <= word2.size(); ++j){if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];else{dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2));}}}return dp[word1.size()][word2.size()];}
};

        其实本题的难点在于考虑的情况是否周全,还是思路要理清。接下来就是重头戏编辑距离题目,我们这道题目就分享到这里。

第三题对应力扣编号为72的题目编辑距离

        在刷题过程中我似乎记得我们前面刷的题都是为这个题目服务的,其实上一个题目我们只涉及到了字符串的删除没有涉及到替换添加等操作,那我们就一起来看看这道题目的具体要求:

       我们其实是求将前面的字符串变成后面字符串的的最少操作数,当然本题目我们可以进行替换删除插入操作,而我们前面的题目只能进行删除操作,那我们需要考虑的情况可能就更多了,我们就使用动规五部曲来解决一下这道题目:

       第一步就是确定 确定dp数组以及下标的含义,其实这个似乎不难,我们完全有经验,dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。跟我们以前的题目定义dp数组的思路是完全类似的。

       第二步就是确定递推公式,这个或许有难度,这个题如果我们要操作的话我们就必须考虑插入删除替换等各种操作,如果对应元素相同那么就不需要任何的操作,但是如果不同的话就麻烦了,其实有如下几个操作,操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。这里就有问题了为什么都是删除元素为什么没有添加元素的操作,其实大家可以变相思考一下,我word2添加一个元素,相当于word1删除一个元素,比如说word1 = "ad", word2 = "a",那我word1删除一个"d"变成"a",与word2添加一个"d",变成"ad"的最后的操作数应该是一样的,操作三:替换元素,这个其实word1替换word1[i - 1],使其与word2[j - 1]相同, 那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同,因此这时候dp[i][j] = dp[i-1][j - 1] +1,那这样我们的递推公式就知道了,就是分为对应元素相等跟不相等的情况。

      第三步就是dp数组的初始化,应该重点思考dp[i][0] 和 dp[0][j] 表示什么?其实也不难,dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; 同理dp[0][j] = j; 这个其实我们上一道题目就涉及到了。

      第四步就是遍历顺序,其实我们也不多说了跟上一道题目是完全一样的。

      这样经过我们的分析我们就给出本题的解题代码:

class Solution {
public:int minDistance(string word1, string word2) {//初始化dp数组vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));//dp数组初始化for (int i = 0; i <= word1.size(); ++i) dp[i][0] = i;for (int j = 0; j <= word2.size(); ++j) dp[0][j] = j;for (int i = 1; i <= word1.size(); ++i){for (int j = 1; j <= word2.size(); ++j){if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];else dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1));}}return dp[word1.size()][word2.size()];}
};

        这道题目很有趣,完全就是动态规划的典型例题,大家一定要把这道题目掌握扎实,我感觉好多公司面试都会考察这道题目,其实我给大家一个总结大家重点首先理解dp数组,其次搞清楚我们需要什么样的操作就可以了,有的我们只可以进行删除操作,有的我们增删改查都可以,而且大家要明白每一种操作我们的状态转移方程应该如何表示就可以,其实我们前面的几道题都是为了编辑距离这道题目服务的,可是说是绝杀。大家务必学明白!!

今日总结

        我们即将到达动态规划的终结篇了,我们还有单调栈跟图论我们的训练营就结束了,大家收获一定是满满的,我们在众多章节收获了很多,大家需要二刷甚至三刷才可以,重要的是理解思想,多去思考多去尝试就可以,我们今天的分享就到这里,大家明天见!

       

相关文章:

  • 卷积神经网络(CNN):原理、架构与实战
  • 火山引擎声音复刻
  • 【Python】2. 基础语法(2)
  • KV Cache:大模型推理加速的核心机制
  • 八、【状态管理篇】:Pinia 在大型应用中的状态管理实践
  • mediapipe标注视频姿态关键点(基础版加进阶版)
  • SE91 找到报错的程序
  • MySQL的参数 innodb_force_recovery 详解
  • 研发中的隐形瓶颈:知识为何越来越难被留下?
  • 清理skywalking历史索引
  • C++:设计模式--工厂模式
  • 【MySQL】第11节|MySQL 8.0 主从复制原理分析与实战
  • 看fp脚本学习的知识1
  • vmvare 虚拟机内存不足
  • atomic.Value与sync.map有什么区?
  • Navicat 17 SQL 预览时表名异常右键表名,点击设计表->SQL预览->另存为的SQL预览时,表名都是 Untitled。
  • 02.【Qt开发】Qt Creator介绍及新建项目流程
  • 跳表(Skip List)查找算法详解
  • 豆包AI一键生成短视频脚本,内容创作更高效
  • 【git】 pull + rebase 或 pull + merge什么区别?
  • 爱站网排名/seo博客教程
  • webstorm可以做网站吗/李守洪
  • mysql做网站/全世界足球排名国家
  • 北京网站建设在哪里天/b站推广入口2023mmm无病毒
  • 网站建设中的风险/会计培训机构排名前十
  • 上传网站安装教程视频/网络运营好学吗