Day48 | 动态规划 :线性DP 编辑距离
本次题解参考自灵神的做法,大家也多多支持灵神的题解
动态规划学习:
1.思考回溯法(深度优先遍历)怎么写
注意要画树形结构图
2.转成记忆化搜索
看哪些地方是重复计算的,怎么用记忆化搜索给顶替掉这些重复计算
3.把记忆化搜索翻译成动态规划
基本就是1:1转换
文章目录
- [Day48 | 动态规划 :线性DP 编辑距离](#Day48 | 动态规划 :线性DP 编辑距离)
-
- 72.编辑距离
-
- 思路分析(子问题):
- [1.回溯 DFS](#1.回溯 DFS)
- 2.记忆化搜索
- 3.1:1翻译为动态规划
72.编辑距离
不知道怎么回事从困难题目直接变成中等题目了
思路分析(子问题):
设两个字符串分别是s和t
对应的最后一个字母分别是x和y
dfs(x,y)那就是s以x结尾,t以y结尾的两个字符串由s变成t的需要的最小操作数了
举个例子
cpp
word1 = "horse", word2 = "ros"
x==1,y==2那就是s==ho变到t==ros需要的最小操作数了
可以先看看昨天这篇题解
Day47 | 动态规划 :线性DP 最长公共子序列&&最长公共子数组-CSDN博客
cpp
//设两个字符串分别是s和t
//对应的最后一个字母分别是x和y
dfs(x,y)那就是s以x结尾,t以y结尾的两个字符串的最长公共子序列的长度了
那就有四种情况
1.选x这个字母也选y这个字母
2.不选x不选y
3.选x不选y
4.选y不选x
如果s[x]==t[y],那肯定就是选x也选y,那肯定就是
dfs(x,y)=dfs(x-1,y-1)+1
如果说s[x]!=t[y],那么就是剩下三种情况里面取一个最大值
dfs(x,y)=max(dfs(x-1,y-1),dfs(x,y-1),dfs(x-1,y))
再仔细一看,其实
dfs(x,y-1),dfs(x-1,y)包含了dfs(x-1,y-1),所以不需要这个了
dfs(x,y)=max(dfs(x,y-1),dfs(x-1,y))
不明白可以举例
dfs(x,y-1)=max(dfs(x-1,y-1),dfs(x,y-2))
其中包含了x-1,y-1的情况
现在重新思考一下选或不选这个过程
1.s[x]==t[y]
那说明我们什么都不用做,因为这两个字符本身就是相等的,我们不需要删除也不需要插入也不需要替换
cpp
dfs(x,y)=dfs(x-1,y-1)
2.s[x]!=t[y]
那我们需要在插入,删除,替换这三种操作中选择一个最小值,选择完最小值之后要+1,因为我们肯定会进行这三种操作的一种
cpp
dfs(x,y)=min(dfs(x,y-1),dfs(x-1,y),dfs(x-1,y-1))+1
// 插入 删除 替换
插入操作理解:
在s中插入一个字母z让它和t的第y个字母相同
(可以这么理解:因为是s要变成t,插入是在x的下一位置(x+1处)插入,我们在x+1处插入了t[y]使得这两个字符串相等,所以我们下次递归的时候就不需要看t[y]了,直接从t[y-1]开始,因为t[y]和s[x+1]是相等的)
dfs(x,y-1)+1
举个例子
s=exection -> t=execution (插入 'u')
从后往前递归,递归到x=3,y=4,即x指向c,y指向u的时候
dfs(x,y)=dfs(3,4)的意思就是
exec变成execu需要的最小操作数
dfs(x,y-1)=dfs(3,3)
exec变成exec需要的操作数
dfs(x,y)=dfs(x,y-1)+1
意思就是
exec变成exec需要的操作数+在s中插入一个u这个操作
删除操作理解:
dfs(x-1,y)+1
在s中删除一个字母,就是把和t[y]不一样的那个字母给删掉
和插入同理
(可以这么理解:因为是s要变成t,删除是删s[x],我们在x处删除了s[x],所以我们下次递归的时候就不需要看s[x]了,直接从s[x-1]开始)
举例:
s=rorse -> t=rose (删除 'r')
那么
dfs(2,1)
s=ror -> t=ro (删除 'r')
dfs(1,1)+1
s=ro -> t=ro
替换操作理解:
dfs(x-1,y-1)+1
把s[x]换成和t[y]一样的字母
这个比较简明,就是用y替换掉了x,然后就都不看这两个了,直接去看两个字符串前面了
1.回溯 DFS
1.返回值和参数
dfs(i,j)那就是s以i结尾,t以j结尾的两个字符串由s变成t的需要的最小操作数了
cpp
int dfs(int i,int j,string &s,string &t)
2.终止条件
s为空字符串,那s就需要插入j+1个字符变成t
t为空字符串,那么s就需要删除i+1个字符变成t
cpp
if(i<0)
return j+1;
if(j<0)
return i+1;
3.本层逻辑
相等就不需要操作,不相等就三种操作里面选最小值
cpp
if(s[i]==t[j])
return dfs(i-1,j-1,s,t);
else
return min(min(dfs(i,j-1,s,t),dfs(i-1,j,s,t)),dfs(i-1,j-1,s,t))+1;
完整代码:
当然,这是超时的
cpp
class Solution {
public:
int dfs(int i,int j,string &s,string &t,vector<vector<int>>& dp)
{
if(i<0)
return j+1;
if(j<0)
return i+1;
if(dp[i][j]!=-1)
return dp[i][j];
if(s[i]==t[j])
return dp[i][j]=dfs(i-1,j-1,s,t,dp);
else
return dp[i][j]=min(min(dfs(i,j-1,s,t,dp),dfs(i-1,j,s,t,dp)),dfs(i-1,j-1,s,t,dp))+1;
}
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size(),vector<int>(word2.size(),-1));
return dfs(word1.size()-1,word2.size()-1,word1,word2,dp);
}
};
cpp
//lambda
class Solution {
public:
int minDistance(string word1, string word2) {
function<int(int,int)> dfs=[&](int i,int j)->int{
if(i<0)
return j+1;
if(j<0)
return i+1;
if(word1[i]==word2[j])
return dfs(i-1,j-1);
else
return min(min(dfs(i,j-1),dfs(i-1,j)),dfs(i-1,j-1))+1;
};
return dfs(word1.size()-1,word2.size()-1);
}
};
2.记忆化搜索
就是搞一个哈希表dp,全都初始化为-1,每次返回前给哈希表dp赋值,碰到不是-1的那就是算过的,那就直接返回计算过的结果,不需要再次递归了
cpp
class Solution {
public:
int dfs(int i,int j,string &s,string &t,vector<vector<int>> &dp)
{
if(i<0||j<0)
return 0;
if(dp[i][j]!=-1)
return dp[i][j];
if(s[i]==t[j])
return dp[i][j]=dfs(i-1,j-1,s,t,dp)+1;
else
return dp[i][j]=max(dfs(i-1,j,s,t,dp),dfs(i,j-1,s,t,dp));
}
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size(),vector<int>(text2.size(),-1));
return dfs(text1.size()-1,text2.size()-1,text1,text2,dp);
}
};
cpp
//lambda
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size(),vector<int>(word2.size(),-1));
function<int(int,int)> dfs=[&](int i,int j)->int{
if(i<0)
return j+1;
if(j<0)
return i+1;
if(dp[i][j]!=-1)
return dp[i][j];
if(word1[i]==word2[j])
return dp[i][j]=dfs(i-1,j-1);
else
return dp[i][j]=min(min(dfs(i,j-1),dfs(i-1,j)),dfs(i-1,j-1))+1;
};
return dfs(word1.size()-1,word2.size()-1);
}
};
3.1:1翻译为动态规划
1.确定dp数组以及下标的含义
dp[i][j]就是dfs(I,j)
下标从1开始,下标0记录对应回溯终止条件处的赋值
2.确定递推公式
和dfs中也是对应的
cpp
if(word1[i]==word2[j])
dp[i+1][j+1]=dp[i][j];
else
dp[i+1][j+1]=min(min(dp[i+1][j],dp[i][j+1]),dp[i][j])+1;
3.dp数组如何初始化
根据回溯终止条件进行初始化
s从空变到t就是插入i哥
t为空,s变过去就是删除i个
注意我们的下标是从1开始的,所以初始化时候要写小于等于,不然会有两个值没有初始化导致报错
cpp
vector<vector<int>> dp(word1.size()+1,vector<int>(word2.size()+1));
for(int i=0;i<=word2.size();i++)
dp[0][i]=i;
for(int i=0;i<=word1.size();i++)
dp[i][0]=i;
4.确定遍历顺序
cpp
for(int i=0;i<word1.size();i++)
for(int j=0;j<word2.size();j++)
if(word1[i]==word2[j])
dp[i+1][j+1]=dp[i][j];
else
dp[i+1][j+1]=min(min(dp[i+1][j],dp[i][j+1]),dp[i][j])+1;
完整代码
cpp
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1,vector<int>(word2.size()+1));
for(int i=0;i<=word2.size();i++)
dp[0][i]=i;
for(int i=0;i<=word1.size();i++)
dp[i][0]=i;
for(int i=0;i<word1.size();i++)
for(int j=0;j<word2.size();j++)
if(word1[i]==word2[j])
dp[i+1][j+1]=dp[i][j];
else
dp[i+1][j+1]=min(min(dp[i+1][j],dp[i][j+1]),dp[i][j])+1;
return dp[word1.size()][word2.size()];
}
};