代码随想录算法训练营Day39 | Leetcode 115.不同的子序列、583.两个字符串的删除操作、72.编辑距离
一、不同的子序列
相关题目:Leetcode115
文档讲解:Leetcode115
视频讲解:Leetcode115
1. Leetcode115.不同的子序列
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
测试用例保证结果在 32 位有符号整数范围内。
提示:
- 1 <= s.length, t.length <= 1000
- 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 子串和 t 子串的最后一位字母,所以只需要 dp[i-1][j-1]。
- 一部分是不用 s[i - 1] 来匹配,个数为 dp[i - 1][j]。
例如: s:bagg 和 t:bag ,s[3] 和 t[2] 是相同的,s 可以用 s[3] 来匹配,即:s[0]s[1]s[3] 组成的 bag;也可以不用 s[3] 来匹配,即: s[0]s[1]s[2] 组成的 bag。所以 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]。
-
当 s[i - 1] 与 t[j - 1] 不相等时,dp[i][j] 只由一部分组成,不用 s[i - 1] 来匹配(相当于在 s 中删除这个元素),即:dp[i - 1][j]。所以递推公式为:dp[i][j] = dp[i - 1][j]。
-
- dp 数组如何初始化:从递推公式可以看出 dp[i][j] 是从上方和左上方推导而来,那么 dp[i][0] 和 dp[0][j] 是一定要初始化的。
- dp[i][0] 表示以 i-1 为结尾的 s 可以随便删除元素,出现空字符串的个数。那么 dp[i][0] 一定都是 1,因为也就是把以 i-1 为结尾的 s,删除所有元素,出现空字符串的个数就是 1。
- dp[0][j] 表示空字符串 s 可以随便删除元素,出现以 j-1 为结尾的字符串 t 的个数。那么 dp[0][j] 一定都是 0,s 无论如何也变成不了 t。
- 最后要看一个特殊位置即:dp[0][0],其应该是 1,空字符串 s 可以删除 0 个元素,变成空字符串 t。
- 确定遍历顺序:从递推公式可以看出 dp[i][j] 都是根据左上方和正上方推出来的,所以遍历的时候一定是从上到下,从左到右。
- 举例推导 dp 数组:以 s:“baegg”,t:“bag” 为例,推导 dp 数组状态如下:
- 动规五部曲:
-
动规
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0] * (len(t)+1) for _ in range(len(s)+1)]
for i in range(len(s)):
dp[i][0] = 1
for j in range(1, len(t)):
dp[0][j] = 0
for i in range(1, len(s)+1):
for j in range(1, len(t)+1):
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[-1][-1]
二、两个字符串的删除操作
相关题目:Leetcode583
文档讲解:Leetcode583
视频讲解:Leetcode583
1. Leetcode583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
提示:
- 1 <= word1.length, word2.length <= 500
- word1 和 word2 只包含小写英文字母
-
思路:
- 动规五部曲:
-
确定 dp 数组以及下标的含义:dp[i][j] 表示以 i-1 为结尾的字符串 word1,和以 j-1 位结尾的字符串 word2,想要达到相等,所需要删除元素的最少次数。
-
确定递推公式:
- 当 word1[i - 1] 与 word2[j - 1] 相同的时候,dp[i][j] = dp[i - 1][j - 1];
- 当 word1[i - 1] 与 word2[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[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(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] 同理。
-
确定遍历顺序:从递推公式可以看出 dp[i][j] 都是根据左上方、正上方、正左方推出来的,所以遍历的时候一定是从上到下,从左到右。
-
举例推导 dp 数组:以 word1:“sea”,word2:“eat” 为例,推导 dp 数组状态图如下:
-
- 动态规划方法二:
- 本题和 Leetcode1143.最长公共子序列 基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
- 动规五部曲:
-
动规方法一
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)]
for i in range(len(word1)+1):
dp[i][0] = i
for j in range(len(word2)+1):
dp[0][j] = j
for i in range(1, len(word1)+1):
for j in range(1, len(word2)+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)
return dp[-1][-1]
- 动规方法二
class Solution(object):
def minDistance(self, word1, word2):
m, n = len(word1), len(word2)
# dp 求解两字符串最长公共子序列
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# 删去最长公共子序列以外元素
return m + n - 2 * dp[-1][-1]
三、编辑距离
相关题目:Leetcode72
文档讲解:Leetcode72
视频讲解:Leetcode72
1. Leetcode72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
提示:
- 0 <= word1.length, word2.length <= 500
- word1 和 word2 由小写英文字母组成
- 思路:
- 动规五部曲:
-
确定 dp 数组以及下标的含义:dp[i][j] 表示以下标 i-1为结尾的字符串 word1,和以下标 j-1 为结尾的字符串 word2,最近编辑距离为 dp[i][j]。
-
确定递推公式:一共有两种情况:
- word1[i - 1] 与 word2[j - 1] 相等:此时不需要编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即 dp[i][j] = dp[i - 1][j - 1];
- word1[i - 1] 与 word2[j - 1] 不等:此时就需要编辑,共三种操作:
- 操作一:word1 删除一个元素,那么就是以下标 i - 2 为结尾的 word1 与 j-1为结尾的 word2 的最近编辑距离再加上一个操作,即 dp[i][j] = dp[i - 1][j] + 1(word2 添加一个元素相当于 word1 删除一个元素);
- 操作二:word2 删除一个元素,那么就是以下标 i - 1 为结尾的 word1 与 j-2 为结尾的 word2 的最近编辑距离再加上一个操作,即 dp[i][j] = dp[i][j - 1] + 1;
- 操作三:替换元素,替换 word1[i - 1],使其与 word2[j - 1] 相同,此时不用增删加元素。只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同,所以 dp[i][j] = dp[i - 1][j - 1] + 1;
综上,当 word1[i - 1] 与 word2[j - 1] 不等时,dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1。
-
dp 数组如何初始化:dp[i][0] 表示以下标 i-1 为结尾的字符串 word1 和空字符串 word2 的最近编辑距离为 dp[i][0]。那么应该对 word1 里的元素全部做删除操作,即:dp[i][0] = i;同理 dp[0][j] = j。
-
确定遍历顺序:从递推公式可以看出 dp[i][j] 是依赖左方,上方和左上方元素的,所以在 dp 矩阵中一定是从左到右从上到下去遍历。
-
举例推导 dp 数组:以输入 word1 = “horse”, word2 = “ros” 为例,dp 矩阵状态图如下:
-
- 动规五部曲:
- 动规
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)]
for i in range(len(word1)+1):
dp[i][0] = i
for j in range(len(word2)+1):
dp[0][j] = j
for i in range(1, len(word1)+1):
for j in range(1, len(word2)+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
return dp[-1][-1]