数据结构与算法|算法总结|动态规划之编辑距离总结篇

啥也不说先上图!

在这里插入图片描述

本节一共总结了六道题,个人认为作为二维dp的典型用法,所以归纳到了一起,方便对比、类比学习。

718.最长重复子序列

力扣题目链接
文章链接
两个整数数组A、B,返回两个数组中 公共的、长度最长的子数组的长度。

从题目我们可以看出,本题将来和后面最大的不一样就是要求公共的子数组。其实就是要求中间是不能断开的。所以这就决定了我们遇到当前元素不想等时,我们直接不管,然后就是需要一个result来实时存储当前的最大公共子数组的长度。

dp[i][j] :以下标i - 1为结尾的num1,和以下标j - 1为结尾的num2,最长重复子数组长度为dp[i][j]
为什么要定义成i-1结尾和以j-1结尾呢?就是为了让初始化比较方便,在后续的子序列的二维dp中,都是采用这样的形式

递推公式:
dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。

  • if (nums[i - 1] == nums2[j - 1])
    • 让最长重复子数组+1
    • 用result保存当前结果
  • if(nums[i - 1] != nums2[j - 1])
    • 直接不管!

即当nums[i - 1]nums2[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1

根据递推公式可以看出,遍历i 和 j 要从1开始!

if (nums1[i - 1] == nums2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + 1;

1143.最长公共子序列

力扣题目链接
文章链接
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
本题跟上面那道【718.最长重复子序列】只不过是不要求是连续起来的。
既然不要求连续,我们这里可以想象我们需要做删除操作(编辑距离思路)

dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

  • if (test[i - 1] == text2[j - 1]
    • 我们还是记录最长公共子序列 + 1
  • if (test[i - 1] != text2[j - 1]
    • 当前的不匹配?那就只能考虑text1的前一个或者考虑text2的前一个了,取一个最大的!
    • 不同时,说明当前字符不能同时出现在最长公共子序列中,因此最长公共子序列的长度取决于去掉text1的最后一个字符和去掉text2的最后一个字符后的两个子问题,即 dp[i - 1][j] 和 dp[i][j - 1] 的最大值。
    • 本质上就是把之前可能相等的信息继承过来

状态转移公式:

if (test[i - 1] == text2[j - 1] {
	dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
	dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}

45.不相交的线

真正做到了跟1143最长公共子序列一模一样
力扣题目链接
文章链接

392.判断子序列

力扣题目链接
文章链接
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

本题其实最合适的算法是双指针和贪心。但是它太适合用来做编辑距离的开篇了。

这里重点讲解它的dp数组和递推公式的推导:

dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。如果我们到时候记录到

这里为什么要定义成下标i-1为结尾和以下标j-1为结尾呢?因为如果以i、j结尾,会让初始化的写法非常麻烦

递推公式:

  1. if (s[i - 1] == t[j - 1])
    • t中找到了一个字符在s中也出现了,相同子序列长度+1
  2. if (s[i - 1] != t[j - 1])
    • 相当于t要删除元素,继续匹配。那就直接继承t的前一个元素信息喽
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];

115.不同的子序列

力扣题目链接
文章链接
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

这里我们当然要把dp数组定义成个数
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]

当s[i - 1] 与 t[j - 1]相等时,dp[i][j]也就是说子序列出现的个数可以有两部分组成。

  • 用s[i - 1]来匹配 ,个数为dp[i - 1][j - 1]
  • 不用s[i - 1]来匹配,个数为dp[i - 1][j]
    所以说这里千万不要有什么+1 -1的操作,我们的是否用s[i-1]来匹配就已经包含了出现个数的操作

如果不想等的话,dp[i][j]只有一个部分组成,不用s[i - 1]来匹配,也就是dp[i][j]=dp[i-1][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];
}

583.两个字符的删除操作

力扣题目链接
文章链接
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步数,每步可以删除任意一个字符串中的一个字符。

现在已经是编辑距离直接不装了,明示删除操作使得word1和word2相同。

dp设定的思路还是不变:
dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数

如果相同:直接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。

这里有一个小trick:
dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2。因为其实dp[i][j-1]已经不靠里word2的j-1个字符了,在此基础上,如果再删除woed1的第 i -1个字符,实际就相当于达到了同时删除两个字符的效果,也就是操作数+1,即dp[i][j-1] + 1。

状态转移方程:

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] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
    //dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}

72.编辑距离

力扣题目链接
文章链接
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

已经是一模一眼里就是操作变多了。

编辑距离我们可以增、删、换

dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增
    删
    换
  1. 不操作
    两个对应元素想等的时候直接不操作,继承前面的
    dp[i][j] = dp[i - 1][j - 1]

word1增加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-2为结尾的word1 与 i-1为结尾的word2的最近编辑距离 加上一个增加元素的操作。

即 dp[i][j] = dp[i - 1][j] + 1;

word2添加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个增加元素的操作。

即 dp[i][j] = dp[i][j - 1] + 1;

3. 删
word2添加一个元素,相当于word1删除一个元素,例如 word1 = “ad” ,word2 = “a”,word2添加一个元素d,也就是相当于word1删除一个元素d,操作数是一样!

替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。

即 dp[i][j] = dp[i - 1][j - 1] + 1;

综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;

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], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构与算法的代码实现涉及到很多不同的问题和挑战。以下是其中一些常见的实现示例: 1. 数组:实现一个支持动态扩容的数组,可以使用动态分配内存的方式来实现数组的动态扩容。当数组空间不足时,可以创建一个更大的数组,将原有的数据复制到新数组中。 2. 链表:可以实现单链表、循环链表和双向链表。单链表包含一个指向下一个节点的指针,循环链表的尾节点指向头节点,双向链表每个节点有一个指向前一个节点和后一个节点的指针。 3. 栈:可以使用数组或链表来实现顺序栈和链式栈。顺序栈使用数组来保存数据,链式栈使用链表来保存数据。在模拟浏览器的前进、后退功能时,可以使用两个栈来实现。 4. 队列:可以使用数组或链表来实现顺序队列和链式队列。顺序队列使用数组来保存数据,链式队列使用链表来保存数据。循环队列则在顺序队列的基础上增加了循环利用空间的功能。 5. 递归:递归是一种函数自己调用自己的方法。可以使用递归来实现斐波那契数列的求值、阶乘的计算以及一组数据集合的全排列等。 6. 排序:可以实现归并排序、快速排序、插入排序、冒泡排序和选择排序等。这些排序算法的实现方式各不相同,但都能实现对一组数据的排序。 7. 二分查找:可以实现对有序数组的二分查找算法。二分查找是一种高效的查找方法,可以在对数时间复杂度内找到目标元素。 8. 散列表:可以实现基于链表法解决冲突问题的散列表。散列表是一种根据关键字直接访问数据的数据结构,可以使用散列函数将关键字映射到对应的数组下标。 9. 字符串:可以实现字符集的Trie树和朴素的字符串匹配算法。Trie树是一种用于快速检索和匹配字符串的数据结构,而朴素的字符串匹配算法则是一种简单但效率较低的算法。 10. 二叉树:可以实现二叉查找树,并支持插入、删除和查找操作。还可以实现查找二叉查找树中某个节点的后继、前驱节点以及二叉树的前、中、后序遍历和按层遍历。 11. 堆:可以实现小顶堆、大顶堆和优先级队列。堆是一种特殊的完全二叉树,可以用来高效地找到最大或最小元素。 12. 图:可以实现有向图和无向图的邻接矩阵和邻接表表示方式。还可以实现图的深度优先搜索、广度优先搜索以及Dijkstra算法和A*算法等。 13. 回溯:可以利用回溯算法求解八皇后问题和0-1背包问题等。回溯算法是一种通过不断回退和尝试来搜索解空间的方法。 14. 分治:可以利用分治算法求解一组数据的逆序对个数。分治算法是一种将问题分解为子问题并分别求解的方法。 15. 动态规划:可以用动态规划解决0-1背包问题、最小路径和、莱文斯坦最短编辑距离和最长公共子序列等问题。动态规划是一种通过保存子问题的解来避免重复计算的方法。 以上是一些常见的数据结构和算法的代码实现示例,具体的实现方式和代码细节可能会有所不同。在实际应用中,根据具体的问题和需求,选择合适的数据结构和算法来实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值