给你两个单词 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 = "horse", word2 = "ros"
输出:3
这个例子来说:
如果使用暴力算法,复杂度会上升到指数级别,这是不允许的。
对于这类题目,最好的方法应该是动态规划。
对于单词wordA,wordB的字母的add,delete,replace操作,可以这样分析:
删除单词A的一个字符=增加单词B的一个字符
替换单词A的一个字符=替换单词B的一个字符
反之同理。
所以,实际操作只有三种:
- 给单词A插入字符
- 给单词B插入字符
- 替换单词A或B的字符
这样就把情况减少到了三种,便于分析。
- 给单词A插入字符:如果horse到ro的距离为a,那么horse到ros的距离显然不会超过a+1,因为最多只需要再增加一个字符s就可以达到目的,长度是a+1
- 给单词B 插入字符:如果我们知道
hors
到ros
的编辑距离为b
,那么显然horse
到ros
的编辑距离不会超过b + 1
,原因同上; - 修改单词
A
的一个字符:如果我们知道hors
到ro
的编辑距离为c
,那么显然horse
到ros
的编辑距离不会超过c + 1
,原因同上。
从 horse
变成 ros
的编辑距离应该为 min(a + 1, b + 1, c + 1)
。
那么我们继续分析a,b,c究竟代表什么。
设定一个dp[wordA.length+1][word2.length+1]来存放动态规划的信息。
dp[i][j]表示的是wordA[0...i],wordB[0...j]的编辑距离,这样我们就有了初始的数组信息。
for (int i = 0; i <= length2; i++) {
dp[0][i] = i; //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
}
for (int i = 0; i <= length1; i++) {
dp[i][0] = i; // wordB长度为0的时候,wordA想要到达wordB,需要delete i个字母
}
那么距离a代表什么呢?a代表的是给A插入字符。
给wordA插入字符才能达到dp[i][j],所以a对应的应该是dp[i-1][j],从wordA[0..i-1]这个长度->wordA[0...i]这个长度,dp[i][j]=dp[i-1][j];
所以,距离b代表的是给wordB插入一个字符,那么dp[i][j]=dp[i][j-1]+1;
距离c代表的是给wordA或者B替换一个字符,因为A或者B替换是等价的,现在理解成给A替换就好。在dp[i-1][j-1]时候,如果wordA[i]=wordB[i]那么就不需要替换,dp[i][j]=dp[i-1][j-1];但是wordA[i]!=wordB[j]时候,那么就需要将A的第i个字符替换为B的第j个字符wordA[i]<-wordB[j],dp[i][j]=dp[i-1][j-1]+1。
题目要求的是最小编辑距离,那么我们应该取通过a,b,c三者修改的最小值作为当前dp[i][j]的value.
(图片来自官网)
现在递推公式已经有了,可以开始进行编程。
for (int i = 1; i <= length1; i++) {
for (int j = 1; j <= length2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1]);
} else {
// 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
}
}
(i=0时候代表wordA长度为0的时候,在初始矩阵时候已经赋值。)
矩阵的最终的效果图。dp[lengthA][lengthB]就是返回值。
完整代码如下:
public static int minDistance(String word1, String word2) {
if (word1 == null || word2 == null)
return 0;
if (word1.length() == 0)
return word2.length();
if (word2.length() == 0) {
return word1.length();
}
int length1 = word1.length();
int length2 = word2.length();
int[][] dp = new int[length1 + 1][length2 + 1];
for (int i = 0; i <= length2; i++) {
dp[0][i] = i; //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
}
for (int i = 0; i <= length1; i++) {
dp[i][0] = i;
}
System.out.println("====打印初始矩阵========");
System.out.print(" _ ");
for (int i = 0 ; i < length2;i++){
System.out.print(word2.charAt(i)+" ");
}
System.out.println();
for (int i = 0; i <= word1.length(); i++) {
if (i==0) {
System.out.print("_ ");
}else {
System.out.print(word1.charAt(i-1)+" ");
}
for (int j = 0; j <= word2.length(); j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
System.out.println("====打印结束========");
for (int i = 1; i <= length1; i++) {
for (int j = 1; j <= length2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1]);
} else {
// 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
}
}
System.out.println();
System.out.println("====打印最终矩阵========");
System.out.print(" _ ");
for (int i = 0 ; i < length2;i++){
System.out.print(word2.charAt(i)+" ");
}
System.out.println();
for (int i = 0; i <= word1.length(); i++) {
if (i==0) {
System.out.print("_ ");
}else {
System.out.print(word1.charAt(i-1)+" ");
}
for (int j = 0; j <= word2.length(); j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
System.out.println("======打印结束======");
return dp[length1][length2];
}
那么我们思考一下,如何来优化一下空间,的空间还是有些大。
当前位置(i,j)只和(i-1,j),(i,j-1),(i,j)有关系。
那么求值时候,只需要直到当前位置的左,左上,上这三个位置就可以得到当前的值。
于是,只需要一个一维数组+几个变量就可以完全搞定这个问题。
public static int minDistance2(String word1, String word2) {
if (word1 == null || word2 == null)
return 0;
if (word1.length() == 0)
return word2.length();
if (word2.length() == 0) {
return word1.length();
}
int length1 = word1.length();
int length2 = word2.length();
int[] dp = new int[length2 + 1];
for (int i = 0; i <= length2; i++) {
dp[i] = i; //初始化
}
for (int i = 1; i <= length1; i++) {
// 记录左上角的值
int leftUp = dp[0];
dp[0] = i;
for (int j = 1; j <= length2; j++) {
int up = dp[j];
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// j-1代表的是当前的左边的值,是之前的dp[i][j-1]
// dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
// leftUp代表的是i-1,j-1
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]) + 1, leftUp);
} else {
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftUp) + 1;
}
leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
}
}
return dp[length2];
}
现在的空间优化的其实还不是最好的情况,还可以继续进行空间的优化。
因为刚才dp[lengthB]时候,是按行就行的生成的dp;
也可以按列进行生成,只需要将dp的长度设置为lengthA即可。
于是,dp的长度可以是min(lengthA,lengthB)
所以有如下代码
public static int minDistance2(String word1, String word2) {
if (word1 == null || word2 == null)
return 0;
if (word1.length() == 0)
return word2.length();
if (word2.length() == 0) {
return word1.length();
}
int length1 = word1.length();
int length2 = word2.length();
int minLength, maxLength;
String minWord, maxWord;
if (length1 > length2) {
minLength = length2;
maxLength = length1;
minWord = word2;
maxWord = word1;
} else {
minLength = length1;
maxLength = length2;
minWord = word1;
maxWord = word2;
}
int[] dp = new int[minLength + 1];
for (int i = 0; i <= minLength; i++) {
dp[i] = i; //初始化
}
for (int i = 1; i <= maxLength; i++) {
// 记录左上角的值
int leftUp = dp[0];
dp[0] = i;
for (int j = 1; j <= minLength; j++) {
int up = dp[j];
if (maxWord.charAt(i - 1) == minWord.charAt(j - 1)) {
// j-1代表的是当前的左边的值,是之前的dp[i][j-1]
// dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
// leftUp代表的是i-1,j-1
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]) + 1, leftUp);
} else {
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftUp) + 1;
}
leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
}
}
return dp[minLength];
}
那么已经到了现在这一步了,我们是不是还可以增加一点难度呢?
备注:
时间复杂度O(n*m)O(n∗m),空间复杂度O(n)O(n)。(n,m代表两个字符串长度)
稍后更。
这个题目相较于上一题主要难在需要弄清楚
[i][j-1],[i-1][j],[i-1][j-1]位置与ic,dc,rc的对应关系。
用一个例子来说明,
ic=5,dc=3,rc=100,wordA=abc,wordB=abc,则过程如下图
public static long minDistance3(String word1, String word2, int ic, int dc, int rc) {
if (word1 == null || word2 == null)
return 0;
int length1 = word1.length();
int length2 = word2.length();
int[][] dp = new int[length1 + 1][length2 + 1];
for (int i = 0; i <= length2; i++) {
dp[0][i] = i * ic; //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
}
for (int i = 0; i <= length1; i++) {
dp[i][0] = i * dc; // wordB长度为0的时候,想要到达wordA,只需要add增加i个字母
}
for (int i = 1; i <= length1; i++) {
for (int j = 1; j <= length2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j] + dc, dp[i][j - 1] + ic), dp[i - 1][j - 1]);
} else {
// 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
dp[i][j] = Math.min(Math.min(dp[i - 1][j] + dc, dp[i][j - 1] + ic), dp[i - 1][j - 1] + rc);
}
}
}
return dp[length1][length2];
}
按之前的思路压缩空间之后的效果是:
public static int minDistance4(String word1, String word2, int ic, int dc, int rc) {
if (word1 == null || word2 == null)
return 0;
int length1 = word1.length();
int length2 = word2.length();
int minLength, maxLength;
String minWord, maxWord;
if (length1 > length2) {
minLength = length2;
maxLength = length1;
minWord = word2;
maxWord = word1;
} else {
minLength = length1;
maxLength = length2;
minWord = word1;
maxWord = word2;
// 现在是wordB代表的是行,问题转换成了从wordB需要多少代价得到wordA
// 所以需要交换插入和删除的代价
int t = ic;
ic = dc;
dc = t;
}
int[] dp = new int[minLength + 1];
for (int i = 0; i <= minLength; i++) {
dp[i] = i * ic; //初始化
}
for (int i = 1; i <= maxLength; i++) {
// 记录左上角的值
int leftUp = dp[0];
dp[0] = i * dc;
for (int j = 1; j <= minLength; j++) {
int up = dp[j];
if (maxWord.charAt(i - 1) == minWord.charAt(j - 1)) {
// j-1代表的是当前的左边的值,是之前的dp[i][j-1]
// dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
// leftUp代表的是i-1,j-1
dp[j] = Math.min(Math.min(dp[j - 1] + ic, dp[j] + dc), leftUp);
} else {
dp[j] = Math.min(Math.min(dp[j - 1] + ic, dp[j] + dc), leftUp + rc);
}
leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
}
}
return dp[minLength];
}
参考:
https://leetcode-cn.com/problems/edit-distance/
https://leetcode-cn.com/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/
如有问题,欢迎留言。