问题
给你两个单词 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’)
思路
编辑距离终于来了,这道题目如果大家没有了解动态规划的话,会感觉超级复杂
编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离
确定dp数组以及下标的含义
**dp[i][j]**
表示以下标**i-1**
为结尾的字符串**word1**
,和以下标**j-1**
为结尾的字符串**word2**
,最近编辑距离为**dp[i][j]**
- 这里在强调一下:为啥要表示下标
i-1
为结尾的字符串呢,为啥不表示下标i
为结尾的字符串呢?用i
来表示也可以!但我统一以下标i-1
为结尾的字符串,在下面的递归公式中会容易理解一点
确定递推公式
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
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]的定义!
if (word1[i - 1] != word2[j - 1])
,此时就需要编辑了,如何编辑呢?操作一:
word1
增加一个元素,使其word1[i - 1]
与word2[j - 1]
相同,那么就是以下标i-2
为结尾的word1
与j-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;
}
dp数组如何初始化
**dp[i][j]**
表示以下标**i-1**
为结尾的字符串**word1**
,和以下标**j-1**
为结尾的字符串**word2**
,最近编辑距离为**dp[i][j]**
那么
dp[i][0]
和dp[0][j]
表示什么呢?dp[i][0]
:以下标i-1
为结尾的字符串word1
,和空字符串word2
,最近编辑距离为dp[i][0]
- 那么
dp[i][0]
就应该是i
,对空字符串做添加元素的操作就可以了,即:dp[i][0] = i;
- 同理
dp[0][j] = j;
for (int i = 0; i <= word1.length(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.length(); j++) dp[0][j] = j;
确定遍历顺序
从如下四个递推公式:
dp[i][j] = dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = dp[i][j - 1] + 1
dp[i][j] = dp[i - 1][j] + 1
可以看出dp[i][j]是依赖左方,上方和左上方元素的,如图:
所以在dp矩阵中一定是从左到右从上到下去遍历
for (int i = 1; i <= word1.length(); i++) {
for (int j = 1; j <= word2.length(); 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], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
}
}
- 举例推导dp数组
- 以示例1,输入:word1 = “horse”, word2 = “ros”为例,dp矩阵状态图如下:
class Solution {
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length() + 1][word2.length() + 1];
for (int i = 0; i <= word1.length(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.length(); j++) dp[0][j] = j;
for (int i = 1; i <= word1.length(); i++) {
for (int j = 1; j <= word2.length(); j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]),
dp[i - 1][j]) + 1;
}
}
}
return dp[word1.length()][word2.length()];
}
}