leetcode72. 编辑距离
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/edit-distance
题目描述
给你两个单词 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’)
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
解题思路
如果看到这题,刚开始想不到动态规划的状态转移公式,可以先尝试用暴力递归写出暴力尝试,然后去改写成动态规划,
首先看暴力递归如何写,题目交代了。转化有三个操作,删除替换和插入。还有一个隐藏操作,就是字符相同时。要跳过。这样就是一个完整的操作,这几个操作怎么反应到代码中呢,我们用指针卡住两个单词的起始位置或者终点位置,然后一个一个字符去比较,相等,我们就跳过,不等就替换删除或者插入的操作。
//伪代码框架
if (word1.charAt(r1) == word2.charAt(r2)){
//啥都不做,跳过,直接去下一个位置操作。
process(r1 - 1.r2 - 2);
}else {
//插入,插入后,word2 的指针移动,word1的保持不动
process(r1,r2 - 1) + 1;
//删除 ,word1 的指针移动,word2的保持不动
process(r1 - 1,r2) + 1;
//替换 word1 和qword2 指针都移动
process(r1 - 1,r2 - 1) + 1;
//为甚加1,递归后的可能要加上当前字符
}
根据上面伪代码,我们可以进行写出暴力递归的尝试了。只需要补充上base case,是不是很简单,网上很多资料直接上动态规划,看的头大也看不懂。
我们这题,从尾部开始向前去比较操作,你也可以从头往后比较,区别就是,base case .和指针移动方向步一样,你可以自己改写下。
代码演示
/**
* 最小编辑距离
* @param word1
* @param word2
* @return
*/
public int minDistance(String word1, String word2) {
if (word1.length() == 0){
return word2.length();
}
if (word2.length() == 0){
return word1.length();
}
if (word1.equals(word2)){
return 0;
}
int n = word1.length();
int m = word2.length();
int[][]dp = new int[n][m];
return process(word1.toCharArray(),n -1,word2.toCharArray(),m - 1,dp);
}
/**
* 暴力递归加上缓存,减少重复计算,不加缓存,提交会超时
* @param word1
* @param r1 word1 的指针
* @param word2
* @param r2 指针
* @param dp 缓存表
* @return
*/
public int process(char[]word1,int r1,char[]word2,int r2,int[][]dp){
//base case 同时来到-1 ,说明比较完了.返回0
if (r1 == -1 && r2 == -1){
return 0;
}
//base case r1 结束了,剩下就需要操作r2 的长度,r2 是下标,计算长度要加1
if (r1 == -1){
return r2 + 1;
}
//base case r2 结束了,剩下就需要操作r1 的长度,r1 是下标,计算长度要加1
if (r2 == -1){
return r1 + 1;
}
//缓存里有就直接拿
if (dp[r1][r2] != 0){
return dp[r1][r2];
}
int res = -1;
if(word1[r1] == word2[r2]){
//跳过
res = process(word1,r1 - 1,word2,r2 - 1,dp);
}else{
//插入
int p1 = process(word1,r1,word2,r2 - 1,dp);
//删除
int p2 = process(word1,r1 - 1,word2,r2,dp);
//替换
int p3 = process(word1,r1 - 1,word2,r2 - 1,dp);
//要取最小的情况
res = Math.min(Math.min(p1,p2),p3) + 1;
}
//答案保存到缓存里
dp[r1][r2] = res;
return res;
}
动态规划
看懂了暴力递归,改动态规划应该就很容易了,不过还是加个表格演示下。
更容易明白如何初始化dp 表。
我们可以把dp表中两边为0位置时,需要操作的长度就是字符的长度,可以直接填上,剩下位置就可以按暴力递归中的依赖关系去填上了。
代码演示
public int minDistance(String word1, String word2){
if (word1.length() == 0){
return word2.length();
}
if (word2.length() == 0){
return word1.length();
}
if (word1.equals(word2)){
return 0;
}
int n = word1.length();
int m = word2.length();
int[][]dp = new int[n + 1][m + 1];
//初始化第一列
for (int i = 1; i <= n ;i++){
dp[i][0] = i;
}
/初始化第一行
for (int j = 1;j <= m;j++){
dp[0][j] = j;
}
//递归改成表中拿值得过程
for (int i = 1; i <= n;i++){
for (int j = 1; j <= m;j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1];
}else{
//插入
int p1 = dp[i][j - 1];
//删除
int p2 = dp[i - 1][j];
//替换\
int p3 = dp[i - 1][j - 1];
dp[i][j] = Math.min(Math.min(p1,p2),p3) + 1;
}
}
}
return dp[n][m];
}