583. 两个字符串的删除操作 - 力扣(LeetCode)
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat" 输出: 2 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
示例 2:
输入:word1 = "leetcode", word2 = "etco" 输出:4
一、递归搜索 + 保存计算结果 = 记忆化搜索
- 二维memo数组 存储计算过的子问题的结果
class Solution {
public:
// 递归搜索 + 保存计算结果 = 记忆化搜索
// 二维memo数组 存储过的子问题的结果
int minDistance(string s, string t) {
int m = s.size(),n = t.size(),memo[m][n]; // 二维memo数组 存储计算过的子问题的结果;
memset(memo,-1,sizeof(memo));// -1 表示没有访问过
function<int(int,int)> dfs = [&](int i,int j) -> int {
if(i<0) //base case 当i指针越界,此时
return j+1;
if(j<0) //base case
return i+1;
if (memo[i][j] != -1) // memo中有当前遇到的子问题的解,直接拿来返回
return memo[i][j];
if (s[i] == t[j]) {
memo[i][j] = dfs(i-1, j-1);
} else {
// memo[i][j] = min(min(dfs(i-1, j)+1,dfs(i, j-1)+1),dfs(i-1, j-1)+2);
// memo[i][j] = min(dfs(i-1, j)+1,dfs(i, j-1)+1);
memo[i][j] = min(dfs(i-1, j),dfs(i, j-1))+1;
}
return memo[i][j];
};
return dfs(m-1,n-1);
}
}
二、动态规划 与 递归 的区别
- 递归公式
if (s[i] == t[j]) {
memo[i][j] = dfs(i-1, j-1);
} else {
// memo[i][j] = min(min(dfs(i-1, j)+1,dfs(i, j-1)+1),dfs(i-1, j-1)+2);
// memo[i][j] = min(dfs(i-1, j)+1,dfs(i, j-1)+1);
memo[i][j] = min(dfs(i-1, j),dfs(i, j-1))+1;
}
递归是自上而下调用,子问题自下而上被解决,最后解决了整个问题,而dp是从base case 出发,通过在dp数组记录中间结果,自下而上地顺序地解决子问题
- dp解法
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的字符串word1,和以j-1为结尾的字符串word2,想要达到相等,所需要删除元素的最少次数
2.确定递推公式
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
3.dp数组初始化
从递推式可看出,dp[i][0] 和 dp[0][j] 是一定要初始化的
- dp[i][0]:word2为字符串,以 i-1 为结尾的字符串 word1 需要删除 i 个元素才能变成空串,和word2相同
- dp[0][j]:word1为字符串,以 j-1 为结尾的字符串 word2 需要删除 j 个元素才能变成空串,和word1相同
- dp[0][0]=0,因为两个空字符串相同,删除操作为0
for(int i=1;i<=m;++i) dp[i][0] = i;
for(int j=1;j<=n;++j) dp[0][j] = j;
4.确定遍历顺序
从递推公式 dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1; 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据正上方、正左方推出来的,所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。
5.举例推导dp数组
(1)动态规划 二维dp
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
for(int i=1;i<=m;++i) dp[i][0] = i;
for(int j=1;j<=n;++j) dp[0][j] = j;
for(int i=1;i<=m;++i) {
for(int j=1;j<=n;++j) {
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
// else dp[i][j] = min(min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+2);
else dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1);
}
}
return dp[m][n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(m * n)
(2)动态规划 二维dp 优化空间
class Solution {
public:
// 动态规划 二维dp 优化空间
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
// vector<vector<int>> dp(m+1,vector<int>(n+1));
vector<vector<int>> dp(2,vector<int>(n+1));
for(int j=1;j<=n;++j) dp[0][j] = j;
for(int i=1;i<=m;++i) {
dp[i%2][0] = i;
for(int j=1;j<=n;++j) {
if(word1[i-1] == word2[j-1]) dp[i % 2][j] = dp[(i-1)%2][j-1];
else dp[i%2][j] = min(dp[(i-1)%2][j]+1,dp[i%2][j-1]+1);
}
}
return dp[m%2][n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(n)
(3)动态规划 一维dp(滚动数组) 优化空间
class Solution {
public:
// 动态规划 一维dp(滚动数组) 优化空间
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
vector<int> dp(n+1);
for(int j=1;j<=n;++j) dp[j] = j;
for(int i=1;i<=m;++i) {
// pre 代表dp[i-1][0]
int pre = dp[0];
// 初始化当前层的 dp[i][0]
dp[0] = i;
for(int j=1;j<=n;++j) {
int tmp = dp[j];
if(word1[i-1] == word2[j-1]) dp[j] = pre;
else dp[j] = min(dp[j]+1,dp[j-1]+1);
pre = tmp;
}
}
return dp[n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(n)
本题除了这种解法外,还有这种解题思路:先求出最长公共子序列,然后 word1.size() + word2.size() - 两倍的最长公共子序列
求最长公共子序列,可以看我往期的这篇文章:leetCode 1143.最长公共子序列
(1)二维dp
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
for(int i=1;i<=m;++i) {
for(int j=1;j<=n;++j) {
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
return m + n - 2 * dp[m][n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(m * n)
(2)二维dp:优化空间
class Solution {
public:
// 方法二 二维dp 优化空间
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
vector<vector<int>> dp(2,vector<int>(n+1));
for(int i=1;i<=m;++i) {
for(int j=1;j<=n;++j) {
if(word1[i-1] == word2[j-1]) dp[i%2][j] = dp[(i-1)%2][j-1] + 1;
else dp[i%2][j] = max(dp[(i-1)%2][j],dp[i%2][j-1]);
}
}
return m + n - 2 * dp[m%2][n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(n)
(3)一维dp:优化空间
class Solution {
public:
// 方法二 一维dp 优化空间
int minDistance(string word1, string word2) {
int m = word1.size(),n = word2.size();
vector<int> dp(n+1);
for(int i=1;i<=m;++i) {
int pre = dp[0];
for(int j=1;j<=n;++j) {
int tmp = dp[j];
if(word1[i-1] == word2[j-1]) dp[j] = pre + 1;
else dp[j] = max(dp[j],dp[j-1]);
pre = tmp;
}
}
return m + n - 2 * dp[n];
}
};
- 时间复杂度: O(m * n)
- 空间复杂度: O(n)
参考和推荐文章、视频:
代码随想录 (programmercarl.com)
动态规划之子序列,还是为了编辑距离做铺垫 | LeetCode:583.两个字符串的删除操作_哔哩哔哩_bilibili
来自代码随想录课堂的截图: