参考文献 代码随想录
一、不同的子序列
给你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数,结果需要对 109 + 7 取模。
示例 1:
输入:s = "rabbbit", t = "rabbit"输出
:3
解释: 如下所示, 有 3 种可以从 s 中得到"rabbit" 的方案
。rabbbit
rabbbit
rabbbit
示例 2:
输入:s = "babgbag", t = "bag"输出
:5
解释: 如下所示, 有 5 种可以从 s 中得到"bag" 的方案
。babgbag
babgbag
babgbag
babgbag
babgbag
提示:
1 <= s.length, t.length <= 1000
s
和t
由英文字母组成
暴力回溯(超时)
class Solution(object):def __init__(self):self.result = 0def numDistinct(self, s, t):""":type s: str:type t: str:rtype: int"""self.dfs(s, t, '', 0)return self.resultdef dfs(self, s, t, li, startIndex):if li == t:self.result += 1for i in range(startIndex, len(s)):self.dfs(s, t, li + s[i], i + 1)
动规
class Solution(object):def numDistinct(self, s, t):""":type s: str:type t: str:rtype: int"""n, m = len(s), len(t)# 创建 DP 表以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]#我们求的是 s 中有多少个 t,而不是 求t中有多少个s,所以只考虑 s中删除元素的情况,即 不用s[i - 1]来匹配 的情况。dp = [[0] * (m + 1) for _ in range(n + 1)]# 当 t 为空字符串时,s 的所有子串都可以形成空字符串for i in range(n + 1):dp[i][0] = 1# 填充 DP 表for i in range(1, n + 1):for j in range(1, m + 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[n][m]
三、两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat" 输出: 2 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
示例 2:
输入:word1 = "leetcode", word2 = "etco" 输出:4
提示:
1 <= word1.length, word2.length <= 500
word1
和word2
只包含小写英文字母
class Solution(object):def minDistance(self, word1, word2):""":type word1: str:type word2: str:rtype: int"""n = len(word1)m = len(word2)dp = [[0] * (n + 1) for _ in range(m + 1)] # 代表的是i-1word2,j-1word1相同操作最少的次数for i in range(n + 1): #有一个是空字符串,另外一个不是,另外一个字符串有多长,那么就要删除多少次dp[0][i] = ifor j in range(m + 1):dp[j][0] = jfor i in range(1, m + 1):for j in range(1, n + 1):if word1[j - 1] == word2[i - 1]: # 如果当前字符相等,那么就依赖前一个字符dp[i][j] = dp[i - 1][j - 1]else: # 要么删除一个字符串的字符,要么另外一个,或者是全部都删除(跳过)dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)return dp[-1][-1]
求出最长的公共子序列,然后在用2个字符串的长度减去最长的2倍
class Solution(object):def minDistance(self, word1, word2):""":type word1: str:type word2: str:rtype: int"""n = len(word1)m = len(word2)dp = [[0] * (n + 1) for _ in range(m + 1)] # 代表的是i-1word2,j-1word1相同操作最少的次数# for i in range(n + 1): #有一个是空字符串,另外一个不是,另外一个字符串有多长,那么就要删除多少次# dp[0][i] = i# for j in range(m + 1):# dp[j][0] = jresult = 0for i in range(1, m + 1):for j in range(1, n + 1):if word1[j - 1] == word2[i - 1]: # 如果当前字符相等,那么就依赖前一个字符dp[i][j] = dp[i - 1][j - 1] + 1else:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])result = max(result, dp[i][j])return (n + m) - 2 * result
三、编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1
和word2
由小写英文字母组成
class Solution(object):def minDistance(self, word1, word2):""":type word1: str:type word2: str:rtype: int"""n = len(word1)m = len(word2)dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。for i in range(n + 1): #有一个是空字符串,另外一个不是,另外一个字符串有多长,那么就要删除多少次dp[0][i] = ifor j in range(m + 1):dp[j][0] = jfor i in range(1, m + 1):for j in range(1, n + 1):if word1[j - 1] == word2[i - 1]: # 如果当前字符相等,那么就依赖前一个字符dp[i][j] = dp[i - 1][j - 1]else: # 要么删除一个字符串的字符,要么领哇dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)return dp[-1][-1]
四、编辑距离总结篇
判断子序列
动态规划:392.判断子序列 (opens new window)给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
这道题目 其实是可以用双指针或者贪心的的,但是我在开篇的时候就说了这是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。
- if (s[i - 1] == t[j - 1])
- t中找到了一个字符在s中也出现了
- if (s[i - 1] != t[j - 1])
- 相当于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.不同的子序列 (opens new window)给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
本题虽然也只有删除操作,不用考虑替换增加之类的,但相对于动态规划:392.判断子序列 (opens new window)就有难度了,这道题目双指针法可就做不了。
当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。
一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
这里可能有同学不明白了,为什么还要考虑 不用s[i - 1]来匹配,都相同了指定要匹配啊。
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。
当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]
所以递推公式为: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.两个字符串的删除操作 (opens new window)给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步数,每步可以删除任意一个字符串中的一个字符。
本题和动态规划:115.不同的子序列 (opens new window)相比,其实就是两个字符串可以都可以删除了,情况虽说复杂一些,但整体思路是不变的。
- 当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] 与 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});
状态转移方程:
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});
}
#编辑距离
动态规划:72.编辑距离 (opens new window)给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
编辑距离终于来了,有了前面三道题目的铺垫,应该有思路了,本题是两个字符串可以增删改,比 动态规划:判断子序列 (opens new window),动态规划:不同的子序列 (opens new window),动态规划:两个字符串的删除操作 (opens new window)都要复杂的多。
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
- if (word1[i - 1] == word2[j - 1])
- 不操作
- if (word1[i - 1] != word2[j - 1])
- 增
- 删
- 换
也就是如上四种情况。
if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
此时可能有同学有点不明白,为啥要即dp[i][j] = dp[i - 1][j - 1]呢?
那么就在回顾上面讲过的dp[i][j]的定义,word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1] 就是 dp[i][j]了。
在下面的讲解中,如果哪里看不懂,就回想一下dp[i][j]的定义,就明白了。
在整个动规的过程中,最为关键就是正确理解dp[i][j]的定义!
if (word1[i - 1] != word2[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;
这里有同学发现了,怎么都是添加元素,删除元素去哪了。
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;
}