文章目录
- 583. 两个字符串的删除操作
- 抽象化:最长公共子序列的长度
- dp 记录删除元素的数量
 
- 72. 编辑距离
- 编辑距离总结篇
583. 两个字符串的删除操作
题目链接 | 解题思路
本题的第一反应应该是进行最长公共子序列的抽象化,因为删除元素这个操作看上去很复杂。但实际上,也能通过 dp 来记录删除操作,也为后面那道编辑距离打下了基础。
抽象化:最长公共子序列的长度
从两个不同的字符串删除元素直到相同,也可以理解为找到最长公共子序列之后就删除多余的元素。这样的抽象化后,可以直接套用找最长公共子序列的解法,在最后用两个字符串的总长度减去两倍的最长公共子序列长度即可。
class Solution:def minDistance(self, word1: str, word2: str) -> int:dp = [[0] * len(word2) for _ in range(len(word1))]first_row_flag = first_col_flag = Falsefor j in range(len(word2)):if word1[0] == word2[j]:first_row_flag = Trueif first_row_flag:dp[0][j] = 1for i in range(len(word1)):if word1[i] == word2[0]:first_col_flag = Trueif first_col_flag:dp[i][0] = 1for i in range(1, len(word1)):for j in range(1, len(word2)):if word1[i] == word2[j]:dp[i][j] = dp[i-1][j-1] + 1else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])return len(word1) + len(word2) - 2 * dp[-1][-1]
dp 记录删除元素的数量
删除元素看着很复杂,因为从结果上来看,删除这个步骤可以发生在任一字符串的任一位置,没办法通过 dp 找到规律。但实际上,只要在 dp 的递推中正确递推了最优解,那就可以解题。
初始化终于变的复杂了,不能再直接初始化第一行和第一列。对于这样的数据,构造对应 "" 的第0行和第0列才是正道!
- dp 数组的下标含义:dp[i][j]是word1[:i]和word2[:j]能够达到相同需要的最少删除数(这里定义的改变和后面的)
- dp 递推公式: - 如果 word1[i] == word2[j],那么不需要对这两个尾部元素进行任何删除,dp[i][j] = dp[i-1][j-1]
- 否则,必然要至少删掉一个元素才能使得有可能使得两个字符串能够匹配,所以 dp[i][j] = min(dp[i-1][j], dp[i][j-1])- 注意,当然有可能这两个尾部元素都需要删除,但这个情况 dp[i-1][j-1] + 2被包含在了dp[i-1][j], dp[i][j-1]中,可以不需要额外考虑
 
- 注意,当然有可能这两个尾部元素都需要删除,但这个情况 
 
- 如果 
- dp 的初始化: - 对于 word1=""来说,需要删除word2[:j]中所有的元素才行,也就是dp[0][j] = j
- 同理,dp[i][0] = i
 
- 对于 
- dp 遍历顺序:从上到下、从左到右即可
- 举例推导:word1 = "sea", word2 = "eat"
| ‘’ | s | e | a | |
|---|---|---|---|---|
| ‘’ | 0 | 1 | 2 | 3 | 
| e | 1 | 2 | 1 | 2 | 
| a | 2 | 3 | 2 | 1 | 
| t | 3 | 4 | 3 | 2 | 
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] = ifor j in range(len(word2) + 1):dp[0][j] = jfor 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][j-1], dp[i-1][j]) + 1return dp[-1][-1]
72. 编辑距离
题目链接 | 解题思路
-  dp 数组的下标含义: dp[i][j]是将word1[:i]转换成word2[:j]的最少操作数(定义和初始化有点关系,类似于背包问题)
-  dp 递推公式: - 如果 word1[i-1] == word2[j-1],那么各自的结尾元素不需要进行任何操作,直接转换成之前的子问题,dp[i][j] = dp[i-1][j-1]
- 否则,我们显然需要进行一次操作(insert、delete、change)才能转换成之前的子问题 - 如果需要进行 insert,显然我们是希望在 word1[:i]的后面加上一个word2[i-1],这样将当前问题转换成了子问题:“将word1[:i]转换成word2[:j-1]”,dp[i][j] = dp[i][j-1] + 1
- 如果需要进行 delete,显然我们是希望删除 word1[i-1],这样将当前问题转换成了子问题:“将word1[:i-1]转换成word2[:j]”,dp[i][j] = dp[i-1][j] + 1
- 如果需要进行 change,显然我们是希望将 word1[i-1]变成word2[j-1],这样将当前问题转换成了子问题:“将word1[:i-1]转换成word2[:j-1]”,dp[i][j] = dp[i-1][j-1] + 1
 
- 如果需要进行 insert,显然我们是希望在 
- 由于我们需要记录最小操作数,所以 dp[i][j] = min([dp[i-1][j-1], dp[i-1][j], dp[i][j-1]]) + 1
 
- 如果 
-  dp 数组的初始化:本题如果直接进行第一行和第一列的初始化,难度无疑逆天,所以才用了背包问题一样处理 ""的定义来帮助初始化- 对于 i=0 (word1 = ""),将其转化成word2[:j]显然需要j次操作(insert),所以dp[0][j] = j
- 对于 j=0 (word2 = ""),将word2[:i]其转化成""显然需要i次操作(delete),所以dp[i][0] = i
- 这样的初始化比真正处理长度为 1 的 word1, word2要正常多了
 
- 对于 
-  dp 的遍历顺序:递推依赖左上方的元素,从上到下、从左到右即可 
-  举例说明: word1 = "horse", word2 = "ros"''r o s ''0 1 2 3 h 1 1 2 3 o 2 2 1 2 r 3 2 2 2 s 4 3 3 2 e 5 4 4 3 
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] = ifor j in range(len(word2) + 1):dp[0][j] = jfor 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][j-1], dp[i-1][j]]) + 1return dp[-1][-1]
编辑距离总结篇
编辑距离总结
在之前的讲解中,或多或少提到了编辑距离。那些题目或多或少都有一些别的解法,或者看作是重复字数组、重复子序列的变种。但是,实际上他们都能被看作是编辑距离这个终极版本的削弱版本:
- 判断子序列 可以看作:只允许删除 t的元素的情况下能否得到s
- 不同的子序列 可以看作:有多少种删除 t的元素的方法能够得到s
- 两个字符串的删除操作 可以看作:允许删除两个字符串的元素,使其相同所需要的最少操作数
- 编辑距离 就是真正的编辑距离