题目来自LeetCode,链接:编辑距离。具体描述为:给你两个单词 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')
首先需要说明一些操作的等价性:
- 在word1插入==在word2删除,比如word1=xxx,word2 =xxxy,在word1插入y跟在word2删除y是等价的;
- 在word1删除==在word2插入,同上;
- 在word1替换==在word2替换,这个很好理解,你把word1中的x替换为y跟将word2中的y替换为x是等价的。
所以实际上只需要对一个字符串做三种操作就行了,不用对两个字符串都做(否则就有六种操作了)。
首先是暴力法,罗列三种可能的操作,取最小值返回。具体做法是定义一个函数distance(word1, word2, idx1, idx2)
代表当前对比的字符为word1[idx1]
和word2[idx2]
,根据这俩字符是否一样有两种情况:
- 俩字符一样,说明不需要额外操作,直接对比下一个字符,也就是
distance(word1, word2, idx1, idx2)=distance(word1, word2, idx1+1, idx2+1)
- 俩字符不一样,有三种操作可选,我们选择这三种操作中最终操作数较少的那个:
- 在
word1
插入一个字符word2[idx2]
使得跟word2[0:idx2]
匹配上(从0到idx2这一段已经转换成功),所以下一个对比的字符就是word1[idx1]
和word2[idx2+1]
,也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1, idx2+1)
,这里的1就是插入操作的次数; - 在
word1
删除一个字符word1[idx1]
,所以下一个对比的字符就是word1[idx1+1]
和word2[idx2]
,也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1+1, idx2)
; - 在
word1
替换字符word1[idx1]
为word2[idx2]
使得跟word2[0:idx2]
匹配上(从0到idx2这一段已经转换成功),所以下一个对比的字符就是word1[idx1+1]
和word2[idx2+1]
,也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1+1, idx2+1)
;
- 在
JAVA版代码如下:
class Solution {
private int distance(String word1, String word2, int idx1, int idx2) {
if (idx1 == word1.length()) {
return word2.length() - idx2;
}
if (idx2 == word2.length()) {
return word1.length() - idx1;
}
int result = 0;
if (word1.charAt(idx1) == word2.charAt(idx2)) {
result = distance(word1, word2, idx1 + 1, idx2 + 1);
}
else {
int d1 = 1 + distance(word1, word2, idx1 + 1, idx2); //删除word1[idx1]
int d2 = 1 + distance(word1, word2, idx1, idx2 + 1); //word1插入一个字符word2[idx2]
int d3 = 1 + distance(word1, word2, idx1 + 1, idx2 + 1);//word1[idx1]替换为word2[idx2]
result = Math.min(Math.min(d1, d2), d3);
}
return result;
}
public int minDistance(String word1, String word2) {
return distance(word1, word2, 0, 0);
}
}
当然暴力法就超时了,因为上面的代码中我们会重复计算同一个(idx1, idx2)
的二元组,所以可以做一些剪枝操作,用一个矩阵记录计算过的(idx1, idx2)
的值避免重复计算。
JAVA版代码如下:
class Solution {
private int len1;
private int len2;
private int[][] record;
private int distance(String word1, String word2, int idx1, int idx2) {
if (idx1 == len1) {
return len2 - idx2;
}
if (idx2 == len2) {
return len1 - idx1;
}
if (record[idx1][idx2] > 0) {
return record[idx1][idx2];
}
int result = 0;
if (word1.charAt(idx1) == word2.charAt(idx2)) {
result = distance(word1, word2, idx1 + 1, idx2 + 1);
}
else {
int d1 = 1 + distance(word1, word2, idx1 + 1, idx2); //删除word1[idx1]
int d2 = 1 + distance(word1, word2, idx1, idx2 + 1); //word1插入一个字符word2[idx2]
int d3 = 1 + distance(word1, word2, idx1 + 1, idx2 + 1);//word1[idx1]替换为word2[idx2]
result = Math.min(Math.min(d1, d2), d3);
}
record[idx1][idx2] = result;
return result;
}
public int minDistance(String word1, String word2) {
len1 = word1.length();
len2 = word2.length();
record = new int[len1][len2];
return distance(word1, word2, 0, 0);
}
}
提交结果如下:
最后是动态规划的方法,其实就是将上面的递归改成递推。假设用一个二维数组dp
记录需要的操作数,dp[i][j]
表示将word1
的前i个字符转换为word2
的前j个字符需要的操作数。首先观察初始状态,也就是一个字符串为空,那么很明显,需要的操作数就是另一个字符串的长度(插入/删除的次数),也就是dp[0][j]=j
以及dp[i][0]=i
。然后需要递推公式,同样需要分情况:
word1[i]==word2[j]
:dp[i][j]=dp[i-1][j-1]
,因为word1[i]
和word2[j]
一样,所以不需要额外操作即可完成转换,需要的操作数等于将word1
的前i-1个字符转换为word2
的前j-1个字符需要的操作数;word1[i]!=word2[j]
:dp[i][j]=1+min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1])
,因为word1[i]
和word2[j]
不一样,所以现在有三种操作(我们选择这三个中需要操作数最少的那个):word1
在第i个字符之后插入字符word2[j]
,需要的操作数等于将word1
的前i个字符转换为word2
的前j-1个字符需要的操作数再加上1(即插入操作),也就是1+dp[i][j-1]
;word1
删除第i个字符,需要的操作数等于将word1
的前i-1个字符转换为word2
的前j个字符需要的操作数再加上1(即删除操作),也就是1+dp[i-1][j]
;word1
第i个字符替换为word2
第j个字符,需要的操作数等于将word1
的前i-1个字符转换为word2
的前j-1个字符需要的操作数再加上1(即替换操作),也就是1+dp[i-1][j-1]
;
动态规划方法的时间复杂度为 ,空间复杂度也是 ,其中m、n分别为两个字符串的长度。
JAVA版代码如下:
class Solution {
public int minDistance(String word1, String word2) {
char[] char1 = word1.toCharArray();
char[] char2 = word2.toCharArray();
int[][] dp = new int[char1.length + 1][char2.length + 1];
for (int i = 1; i <= char1.length; ++i) {
dp[i][0] = i;
}
for (int j = 1; j <= char2.length; ++j) {
dp[0][j] = j;
}
for (int i = 1; i <= char1.length; ++i) {
for (int j = 1; j <= char2.length; ++j) {
if (char1[i - 1] == char2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
}
}
}
return dp[char1.length][char2.length];
}
}
提交结果如下:
然后进一步考虑到每个状态dp[i][j]
都只与dp[i-1][j]
、dp[i][j-1]
以及dp[i-1][j-1]
有关,所以可以进一步减少空间复杂度,只用一维数组保存需要的dp[i-1][j]
以及dp[i][j-1]
,单独用一个变量保存dp[i-1][j-1]
,从而将空间复杂度降低到
。
JAVA版代码如下:
class Solution {
public int minDistance(String word1, String word2) {
char[] char1 = word1.toCharArray();
char[] char2 = word2.toCharArray();
int[] dp = new int[char2.length + 1];
for (int j = 1; j <= char2.length; ++j) {
dp[j] = j;
}
for (int i = 1; i <= char1.length; ++i) {
int dp_ij_1 = i - 1;
dp[0] = i;
for (int j = 1; j <= char2.length; ++j) {
int temp = dp[j];
if (char1[i - 1] == char2[j - 1]) {
dp[j] = dp_ij_1;
}
else {
dp[j] = 1 + Math.min(Math.min(dp[j], dp[j - 1]), dp_ij_1);
}
dp_ij_1 = temp;
}
}
return dp[char2.length];
}
}
提交结果如下:
Python版代码如下:
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
len1 = len(word1)
len2 = len(word2)
dp = [i for i in range(len2 + 1)]
for i in range(1, len1 + 1):
dp_ij_1 = dp[0]
dp[0] = i
for j in range(1, len2 + 1):
temp = dp[j]
if word1[i - 1] == word2[j - 1]:
dp[j] = dp_ij_1
else:
dp[j] = 1 + min(dp[j - 1], dp[j], dp_ij_1)
dp_ij_1 = temp
return dp[len2]
提交结果如下: