6.经典动态规划:编辑距离
编辑距离(LeetCode 72题 难度:困难)
编辑距离问题就是给我们两个字符串s1
和s2
,只能用三种操作,让我们把s1
变成s2
,求最少的操作数。需要明确的是,不管是把s1
变成s2
还是反过来,结果都是一样的,所以后文就以s1
变成s2
举例。
思路分析
dp数组的定义
dp[i,j]:就是s1[0...i],和s2[0...j]的最小编辑距离(代价最小)
比如dp[2][4]=2的含义,就是这样的字符串代表长度 S1="ac"和S2=“babc”
解决两个字符串的动态规划问题,一般就是用两个指针i,j,分别指向两个字符串的最后,然后一步一步向前走,缩小问题规模
设两个字符串分别为 "rad" 和 "apple",为了把s1
变成s2
,算法会这样进行:
选择
根据上面的情况,发现 操作不只有三个,其实还有四个操作,那就是啥也不做
比如这种情况 S1[I]==S2[J]
补充
还有一个很容易处理的问题 :
就是j走完了S2,如果i还没有走完的话,只能删除s1的多余节点了
类似:如果i走完了s1,j还没有走完s2,那就只能用插入操作把s2的剩下的字符全部插入到s1。下面会看到,这两种情况就是算法的base case
代码详解
先梳理一下之前的思路:
base case 是i
走完s1
或j
走完s2
,可以直接返回另一个字符串剩下的长度。
对于每对儿字符s1[i]
和s2[j]
,可以有四种操作:
if s1[i] == s2[j]:
啥都别做(skip)
i, j 同时向前移动
else:
三选一:
插入(insert)
删除(delete)
替换(replace)
有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,理解需要点技巧,先看下代码:
public int minDistance(String word1, String word2) {
char[] s1 = word1.toCharArray();
int m = s1.length;
char[] s2 = word2.toCharArray();
int n = s2.length;
int[][] dp = new int[m+1][n+1];
//base
for (int i = 1; i <=m; i++) {
dp[i][0]=i;
}
for (int i = 1; i <=n; i++) {
dp[0][i]=i;
}
//自底向上求解
for (int i = 1; i <= m; i++) {
for (int j = 1; j <=n ; j++) {
if(s1[i-1]==s2[j-1]){
//如果 相等 就缩小指针 ,转为子问题
dp[i][j]=dp[i-1][j-1];
}else {
//做哪种操作 代价最小
dp[i][j]=Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]+1));
}
}
}
return dp[m][n];
}
dp[i-1][j-1]解释
本来就相等,不需要任何操作,缩小指针
s1[0..i] 和 s2[0..j] 的最小编辑距离等于
s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
也就是说 dp(i, j) 等于 dp(i-1, j-1)
dp(i, j - 1) + 1, # 插入
我直接在 s1[i] 插入一个和 s2[j] 一样的字符
那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
别忘了操作数加一
dp(i - 1, j) + 1, # 删除
我直接把 s[i] 这个字符删掉
前移 i,继续跟 j 对比
操作数加
dp(i - 1, j - 1) + 1 # 替换
我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
同时前移 i,j 继续对比
操作数加一