1.题目描述
Given two strings word1
and word2
, return the minimum number of operations required to convert word1
to word2
.
You have the following three operations permitted on a word:
- Insert a character
- Delete a character
- Replace a character
Example 1:
Input: word1 = "horse", word2 = "ros" Output: 3 Explanation: horse -> rorse (replace 'h' with 'r') rorse -> rose (remove 'r') rose -> ros (remove 'e')
Example 2:
Input: word1 = "intention", word2 = "execution" Output: 5 Explanation: intention -> inention (remove 't') inention -> enention (replace 'i' with 'e') enention -> exention (replace 'n' with 'x') exention -> exection (replace 'n' with 'c') exection -> execution (insert 'u')
Constraints:
0 <= word1.length, word2.length <= 500
word1
andword2
consist of lowercase English letters.
2.题解
方法1:自顶向下(递归 + 备忘录数组memo[][])
思路:对s1和s2字符串都是从后向前遍历,备忘录数组大小为[m+1][n+1],memo[i][j] 存储了word1(0...i-1)到 word2(0...j-1)的最小编辑距离。
(1)状态:原问题和子问题之间变化的变量,字符串1和字符串2比较长度的变化,即 i 和 j(遍历字符串1和字符串2的下标值);
(2)dp函数的定义:dp(i, j)表示 字符串1(0...i) 和 字符串2(0...j) 之间的最小编辑距离;
(3)确定选择并择优:分情况讨论:如果当前 word1[i] == word2[j],则跳过,进入dp(i-1, j-1),当前编辑距离为0;否则有3种操作,分别是 插入、删除、替换,比较者种操作的编辑距离,选择最小的存入备忘录memo;
(4)base case:若字符串1 先被遍历完,则 距离需要加上字符串2剩余长度(相当于在做插入操作),若字符串2 先被遍历完,则 距离需要加上字符串1剩余长度(相当于在做删除操作)。
class Solution {
public int minDistance(String word1, String word2) {
if(word1.length() == 0){
return word2.length();
}
if(word2.length() == 0){
return word1.length();
}
//开辟备忘录数组
int[][] memo = new int[word1.length()+1][word2.length()+1];
//初始化备忘录
for(int i = 0; i < word1.length()+1; i++){
for(int j = 0; j < word2.length()+1; j++){
if(i == 0){
memo[0][j] = j;
}else if(j == 0){
memo[i][0] = i;
}else{
memo[i][j] = -1;
}
}
}
return dp(word1, word2, word1.length()-1, word2.length()-1, memo);
}
//递归:dp函数
private int dp(String s1, String s2, int i, int j, int[][] memo){
//s1字符串先遍历完,则插入剩余的s2字符串
if(i == -1){
return j+1;
}
//s2字符串先遍历完,则删除剩余的s1字符串
if(j == -1){
return i+1;
}
//如果备忘录里存有结果就直接返回
if(memo[i+1][j+1] != -1){
return memo[i+1][j+1];
}
// s1当前字符 != s2当前字符,比较“插入、删除、替换”3种操作的距离
if(s1.charAt(i) != s2.charAt(j)){
memo[i+1][j+1] = calcMin(
dp(s1, s2, i-1, j-1, memo)+1,
dp(s1, s2, i-1, j, memo)+1,
dp(s1, s2, i, j-1, memo)+1);
}else{
//s1当前字符 == s2当前字符,跳过比较下一个
memo[i+1][j+1] = dp(s1, s2, i-1, j-1, memo);
}
return memo[i+1][j+1];
}
private int calcMin(int a , int b, int c){
return Math.min(a, Math.min(b,c));
}
}
时间复杂度:O(mn),其中m、n分别为word1、word2的长度;
空间复杂度:O(mn),memo数组大小加上递归栈。
方法2:自底向上(dp table)
思路:对s1和s2字符串都是从后向前遍历,dp数组大小为[m+1][n+1],dp[i][j] 存储了word1(0...i-1)到 word2(0...j-1)的最小编辑距离。
这里的dp数组和方法1中的dp函数有些类似。
class Solution {
public int minDistance(String word1, String word2) {
if(word1 == null || word2 == null){
return 0;
}
int m = word1.length();
int n = word2.length();
if(m == 0){
return n;
}
if(n == 0){
return m;
}
int[][] dp = new int[m+1][n+1];
//base case
for(int i = 0; i <= m; i++){
dp[i][0] = i;
}
for(int j = 0; j <= n; j++){
dp[0][j] = j;
}
//遍历字符串1和字符串2,求最小编辑距离
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];
}else{
//对字符串1的操作
int insert = dp[i][j-1]+1;
int delete = dp[i-1][j]+1;
int replace = dp[i-1][j-1]+1;
dp[i][j] = Math.min(insert, Math.min(delete, replace));
}
}
}
return dp[m][n];
}
}
时间复杂度:O(mn),其中m、n分别为word1、word2的长度;
空间复杂度:O(mn),只有开辟dp数组的开销,没有递归函数的栈开销,比方法1开销小一点。
3.收获
1、从这道题开始理解什么是“自顶向下”、什么是“自底向上”。其实递归函数一般是“自顶向下”,因为是由当前原问题进入子问题的递归最后返回解,而dp数组这种动态规划,其实是在已知递归思路后想到的,可以从子问题开始求解,最后得到原问题的解,感觉方法2是方法1的升华,所以动态规划的转移方程以及dp数组定义比较难想到。