目录
一.两个字符串的删除操作
1.对应letecode链接:
题目描述:
解题思路:
方法一:暴力递归⭐⭐⭐⭐⭐
一般这种两个字符串的dp问题为样本对应模型:样本对应模型一般考虑结尾如何如何。下面我们定义递归函数:
//递归含义为:str1的前 i 个字符与word2的前 j个字符相同所需的最小删除操作步数 int process(const string&str1,int i,const string&str2,int j) { }
下面我们来分析可能性:
可能性一:字符串str1删除一个字符剩下的字符和str2相同的最小步数
可能性二:字串串str2删除结尾位置的字符加上剩下字符串变成str1的最小步数:
可能性三:结尾位置的字符相等
如果结尾位置的字符相等那么最小步数就是str1前i-1个字符和str2前j-1个字符的相同最小步数。
可能性四:结尾位置的字符不相等
如果结尾位置的字符不相等那么就需要将str1和str2结尾位置的字符全部删掉+str1前i-1个字符和str2前j-1个字符的相同最小步数。
对应代码:
class Solution { public: int minDistance(string word1, string word2) { return process(word1,word1.size(),word2,word2.size()); } //递归含义为:str1的前 i 个字符与word2的前 j个字符相同所需的最小删除操作步数 int process( string&str1,int i, string&str2,int j) { if(i==0){ return j; } if(j==0) { return i; } //可能性1:str1删除一个字符+剩下的字符串和str2相同的最小代价 int ways1=process(str1,i-1,str2,j)+1; //可能性2:str2删除一个字符+剩下的字符串和str1相同的最小代价 int ways2=process(str1,i,str2,j-1)+1; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int ways3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1):INT_MAX; //可能性4:结尾位置的字符不相等将两个字符串结尾位置的字符全部删掉 int ways4=process(str1,i-1,str2,j-1)+2; return min(min(ways1,ways2),min(ways3,ways4)); } };
方法二:记忆化搜索⭐⭐⭐
记忆化搜索建立在暴力递归的基础之上所以我们只需要定义一张缓存表记录每个位置的结尾下次遇到不用重复计算直接拿结果即可。
对应代码:
class Solution { public: vector<vector<int>>dp; int minDistance(string word1, string word2) { dp.resize(word1.size()+1,vector<int>(word2.size()+1,-1)); return process(word1,word1.size(),word2,word2.size()); } //递归含义为:str1的前 i 个字符与word2的前 j个字符相同所需的最小删除操作步数 int process( string&str1,int i, string&str2,int j) { if(i==0){ return j; } if(j==0) { return i; } if(dp[i][j]!=-1) { return dp[i][j]; } //可能性1:str1删除一个字符+剩下的字符串和str2相同的最小代价 int ways1=process(str1,i-1,str2,j)+1; //可能性2:str2删除一个字符+剩下的字符串和str1相同的最小代价 int ways2=process(str1,i,str2,j-1)+1; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int ways3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1):INT_MAX; //可能性4:结尾位置的字符不相等将两个字符串结尾位置的字符全部删掉 int ways4=process(str1,i-1,str2,j-1)+2; int ans= min(min(ways1,ways2),min(ways3,ways4)); dp[i][j]=ans; return ans; } };
方法三:严格位置依赖的动态规划⭐⭐⭐⭐⭐⭐
严格位置依赖的动态规划其实也是根据暴力递归改过来的,其实就是将暴力递归的终止条件作为初始值。其状态转移方程就是暴力递归中的可能性。由于可能性上面已经说过了在这里就不重复了,具体请看代码。
对应代码:
class Solution { public: int minDistance(string word1, string word2) { int M=word1.size()+1; int N=word2.size()+1; vector<vector<int>>dp(M,vector<int>(N)); for(int i=0;i<N;i++) { dp[0][i]=i; } //初始值根据暴力递归填 for(int j=0;j<M;j++) { dp[j][0]=j; } for(int i=1;i<M;i++) { for(int j=1;j<N;j++) { int ways1=dp[i-1][j]+1; int ways2=dp[i][j-1]+1; //注意这里将可能性2和可能性3合并了 int ways3=word1[i-1]==word2[j-1]?dp[i-1][j-1]:dp[i-1][j-1]+2; dp[i][j]=min(min(ways1,ways2),ways3); } } return dp[M-1][N-1]; } //递归含义为:str1的前 i 个字符与word2的前 j个字符相同所需的最小删除操作步数 int process( string&str1,int i, string&str2,int j) { if(i==0){ return j; } if(j==0) { return i; } //可能性1:str1删除一个字符+剩下的字符串和str2相同的最小代价 int ways1=process(str1,i-1,str2,j)+1; //可能性2:str2删除一个字符+剩下的字符串和str1相同的最小代价 int ways2=process(str1,i,str2,j-1)+1; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int ways3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1):INT_MAX; //可能性4:结尾位置的字符不相等将两个字符串结尾位置的字符全部删掉 int ways4=process(str1,i-1,str2,j-1)+2; int ans= min(min(ways1,ways2),min(ways3,ways4)); return ans; } };
二.编辑距离⭐⭐⭐⭐⭐⭐⭐(被火车撞了也不能忘)
1.对应OJ链接:
2.题目描述:
3.解题思路:
方法一:暴力递归
//递归含义str1取前i个字符编辑成str2取j个字符的最小代价 int process(const string&str1,int i,const string&str2,int j,int ic,int dc,int rc)
递归含义str1取前i个字符编辑成str2取j个字符的最小代价。下面我们来分析可能性:
可能性一:字符串str1结尾位置插入一个str2结尾位置的字符承担一个插入的代价+str1后续字符串编辑成
可能性二:删除str1结尾位置的字符承担一个删除代价+后续字符串编辑成str2的最小代价。
可能性三:字符串str1和str2结尾位置的字符相等此时最小编辑代价为str1前i-1个字符编辑成str2j-1个字符的最小代价
可能性四:字符串str1和str2结尾位置的字符不相等此时只能将str1的字符替换成str2结尾位置的字符承担一个替换代价+后续编辑成str2的最小代价
替换:
对应代码:
#include<iostream> #include<string> #include<limits.h> using namespace std; //递归含义str1取前i个字符编辑成str2取j个字符的最小代价 int process(const string&str1,int i,const string&str2,int j,int ic,int dc,int rc) { if(i==0){//字符串str1为空串此时想要编辑成str2只能插入 return j*ic; } if(j==0){//字符串str2为空串此时只能删除str1的字符 return i*dc; } //可能性1字符串str1结尾位置插入一个str2结尾位置的字符 int p1=process(str1,i,str2,j-1,ic,dc,rc)+ic; //可能性2:删除str1结尾位置的字符 int p2=process(str1,i-1,str2,j,ic,dc,rc)+dc; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int p3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1,ic,dc,rc):INT_MAX; //可能性4:str1结尾位置的字符和str2结尾位置的字符不相等此时需要将str1结尾位置的字符替换成str2结尾位置的字符 int p4=process(str1,i-1,str2,j-1,ic,dc,rc)+rc; return min(min(p1,p2),min(p3,p4)); } int main() { string str1,str2; int ic,dc,rc; cin>>str1>>str2>>ic>>dc>>rc; cout<<process(str1,str1.size(),str2,str2.size(),ic,dc,rc)<<endl; return 0; }
方法二:记忆化搜索⭐⭐⭐⭐⭐
记忆化所示由于前面已经说过了下面只给出代码
#include<iostream> #include<string> #include<limits.h> #include<vector> using namespace std; vector<vector<int>>dp;//用来进行缓存 //递归含义str1取前i个字符编辑成str2取j个字符的最小代价 int process(const string&str1,int i,const string&str2,int j,int ic,int dc,int rc) { if(i==0){//字符串str1为空串此时想要编辑成str2只能插入 return j*ic; } if(j==0){//字符串str2为空串此时只能删除str1的字符 return i*dc; } if(dp[i][j]!=-1){ return dp[i][j]; } //可能性1字符串str1结尾位置插入一个str2结尾位置的字符 int p1=process(str1,i,str2,j-1,ic,dc,rc)+ic; //可能性2:删除str1结尾位置的字符 int p2=process(str1,i-1,str2,j,ic,dc,rc)+dc; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int p3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1,ic,dc,rc):INT_MAX; //可能性4:str1结尾位置的字符和str2结尾位置的字符不相等此时需要将str1结尾位置的字符替换成str2结尾位置的字符 int p4=process(str1,i-1,str2,j-1,ic,dc,rc)+rc; int ans= min(min(p1,p2),min(p3,p4)); dp[i][j]=ans; return ans; } int main() { string str1,str2; int ic,dc,rc; cin>>str1>>str2>>ic>>dc>>rc; dp.resize(str1.size()+1,vector<int>(str2.size()+1,-1)); cout<<process(str1,str1.size(),str2,str2.size(),ic,dc,rc)<<endl; return 0; }
三.严格位置依赖的动态规划
#include<iostream> #include<string> #include<limits.h> #include<vector> using namespace std; //递归含义str1取前i个字符编辑成str2取j个字符的最小代价 int process(const string&str1,int i,const string&str2,int j,int ic,int dc,int rc) { if(i==0){//字符串str1为空串此时想要编辑成str2只能插入 return j*ic; } if(j==0){//字符串str2为空串此时只能删除str1的字符 return i*dc; } //可能性1字符串str1结尾位置插入一个str2结尾位置的字符 int p1=process(str1,i,str2,j-1,ic,dc,rc)+ic; //可能性2:删除str1结尾位置的字符 int p2=process(str1,i-1,str2,j,ic,dc,rc)+dc; //可能性3:str1结尾位置的字符和str2结尾位置的字符相等 int p3=str1[i-1]==str2[j-1]?process(str1,i-1,str2,j-1,ic,dc,rc):INT_MAX; //可能性4:str1结尾位置的字符和str2结尾位置的字符不相等此时需要将str1结尾位置的字符替换成str2结尾位置的字符 int p4=process(str1,i-1,str2,j-1,ic,dc,rc)+rc; int ans= min(min(p1,p2),min(p3,p4)); return ans; } int main() { string str1,str2; int ic,dc,rc; cin>>str1>>str2>>ic>>dc>>rc; int M=str1.size()+1; int N=str2.size()+1; vector<vector<int>>dp(M,vector<int>(N)); for(int i=0;i<M;i++) { dp[i][0]=i*dc; } for(int j=0;j<N;j++) { dp[0][j]=j*ic; } for(int i=1;i<M;i++){ for(int j=1;j<N;j++){ dp[i][j]=min(dp[i][j-1]+ic,dp[i-1][j]+dc); dp[i][j]=min(dp[i][j],str1[i-1]==str2[j-1]?dp[i-1][j-1]:dp[i-1][j-1]+rc); } } cout<<dp[M-1][N-1]<<endl; return 0; }
letecode也有一掉类似的题是这题的弱化版本
题目描述:
只需要将插入替换删除操作的代价改为1即可
class Solution { public: int minDistance(string word1, string word2) { return getMinDistance(word1,word2,1,1,1); } int getMinDistance(string &str1,string&str2,int rd,int ic,int rc ) { int M=str1.size()+1; int N=str2.size()+1; vector<vector<int>>dp(M,vector<int>(N)); //dp[i][j]的含义str1取前i个字符,str2取前j个字符,str1替换为str2的最小代价 for(int i=1;i<N;i++) { dp[0][i]=i*rd; } for(int i=1;i<M;i++) { dp[i][0]=i*ic; } for(int i=1;i<M;i++) { for(int j=1;j<N;j++) { dp[i][j]=str1[i-1]==str2[j-1]?dp[i-1][j-1]:dp[i-1][j-1]+rc; dp[i][j]=min(dp[i][j],min(dp[i-1][j]+rd,dp[i][j-1]+ic)); } } return dp[M-1][N-1]; } };
三.字符串交错问题
1.对应letecode链接
2.题目描述:
3.解题思路:
方法一:暴力递归⭐⭐⭐⭐⭐⭐
//递归含义str1取前i个字符,str2取前j个字符能否组成str3 bool process(string&str1,int i,string&str2,int j,string&str3)
这种字符串匹配题目一般都是讨论结尾位置如何如何。。下面我们来讨论可能性:
s1 和 s2 的字符彼此交错,组成了s3,并且交错的字符个数是不确定的。反过来想,s3 的每个字符是从 s1 和 s2 二者中挑选的。
s3 的每个字符有两个选择:从 s1 选、从 s2 选。所以就有一下可能性。
可能性一:str3结尾位置的字符来自str1
可能性二:str3结尾位置的字符来自str2
三个指针,分别扫描 s1 s2 s3,如下图左例,i、k 指向的字母相同,则选择 s1[i]s1[i] 作为 s3[k]s3[k],i、k 都右移一位,递归剩余子串。
如果 k 最后越界,代表 s3 选完了,成功从 s1 和 s2 中交错选出字母组成自己。
其实k指针我们可以用i和j指针给优化掉既(i+j-1)
对应代码:
class Solution { public: bool isInterleave(string s1, string s2, string s3) { if(s1.size()+s2.size()!=s3.size()){ return false; } return process(s1,s1.size(),s2,s2.size(),s3); } //递归含义str1取前i个字符,str2取前j个字符能否组成str3取i+j个字符 bool process(string&str1,int i,string&str2,int j,string&str3){ if(i==0&&j==0){ return true; } if(i==0){ return str2[j-1]==str3[j-1]&&process(str1,i,str2,j-1,str3); } if(j==0){ return str1[i-1]==str3[i-1]&&process(str1,i-1,str2,j,str3); } //普遍情况 return (str1[i-1]==str3[i+j-1]&&process(str1,i-1,str2,j,str3))|| (str2[j-1]==str3[i+j-1]&&process(str1,i,str2,j-1,str3)); } };
方法二:记忆化搜索:
class Solution { public: vector<vector<int>>dp; bool isInterleave(string s1, string s2, string s3) { if(s1.size()+s2.size()!=s3.size()){ return false; } dp.resize(s1.size()+1,vector<int>(s2.size()+1,-1)); return process(s1,s1.size(),s2,s2.size(),s3); } //递归含义str1取前i个字符,str2取前j个字符能否组成str3取i+j个字符 bool process(string&str1,int i,string&str2,int j,string&str3){ if(i==0&&j==0){ return true; } if(i==0){ return str2[j-1]==str3[j-1]&&process(str1,i,str2,j-1,str3); } if(j==0){ return str1[i-1]==str3[i-1]&&process(str1,i-1,str2,j,str3); } if(dp[i][j]!=-1) return dp[i][j]; bool ans= (str1[i-1]==str3[i+j-1]&&process(str1,i-1,str2,j,str3))|| (str2[j-1]==str3[i+j-1]&&process(str1,i,str2,j-1,str3)); dp[i][j]=ans; return ans; } };
方法三:严格位置依赖的动态规划
class Solution { public: bool isInterleave(string s1, string s2, string s3) { if(s1.size()+s2.size()!=s3.size()) return false; int M=s1.size()+1; int N=s2.size()+1; vector<vector<bool>>dp(M,vector<bool>(N)); //dp[i][j]的含义:s1取前i个,s2取前j个能否搞定s3取前i+j个 dp[0][0]=true; for(int i=1;i<M;i++) { if(s3[i-1]==s1[i-1]) { dp[i][0]=true; } else { break; } } // for(int i=1;i<N;i++) { if(s3[i-1]==s2[i-1]) { dp[0][i]=true; } else { break; } } for(int i=1;i<M;i++) { for(int j=1;j<N;j++) { dp[i][j]=(s1[i-1]==s3[i+j-1]&&dp[i-1][j]) ||(s2[j-1]==s3[i+j-1]&&dp[i][j-1]); } } return dp[M-1][N-1]; } //递归含义str1取前i个字符,str2取前j个字符能否组成str3取i+j个字符 bool process(string&str1,int i,string&str2,int j,string&str3){ if(i==0&&j==0){ return true; } if(i==0){ return str2[j-1]==str3[j-1]&&process(str1,i,str2,j-1,str3); } if(j==0){ return str1[i-1]==str3[i-1]&&process(str1,i-1,str2,j,str3); } bool ans= (str1[i-1]==str3[i+j-1]&&process(str1,i-1,str2,j,str3))|| (str2[j-1]==str3[i+j-1]&&process(str1,i,str2,j-1,str3)); return ans; } };
四.不同的子序列
1.对应letecode链接
2.题目描述
解题思路:
方法一:暴力递归⭐⭐⭐⭐⭐
//递归含义str1[0.....i]范围上能搞定几个str2[0....j] int process(const string&str1,int i,const string&str2,int j)
对于这种子数组子序列问题一般都是结尾位置如何如何。下面我们来分析可能性:
可能性一:str1要当前位置的字符
可能性二:字符串str2要当前i位置的字符。相对上面两题而言还是比较简单的
4.对应代码:
class Solution { public: int numDistinct(string s, string t) { return process(s,s.size()-1,t,t.size()-1); } //递归含义str1[0.....i]范围上能搞定几个str2[0....j] int process(const string&str1,int i,const string&str2,int j) { if(i==0&&j==0){ return str1[i]==str2[j]?1:0; } if(i==0){//str1只有一个字符串但是str2不止一个字符串 return 0; } //str2只有一个字符串 if(j==0){ return str1[i]==str2[j]?process(str1,i-1,str2,j)+1:process(str1,i-1,str2,j); } //可能性一:str1不要i位置的字符 int p1=process(str1,i-1,str2,j); //可能性二:str2要i位置的字符 int p2=str1[i]==str2[j]?process(str1,i-1,str2,j-1):0; //最终的结果为可能性一+可能性二 return p1+p2; } };
方法二:记忆化搜索⭐⭐⭐⭐⭐
由于前面已经介绍过了下面就只给出代码
class Solution { public: vector<vector<int>>dp; int numDistinct(string s, string t) { dp.resize(s.size(),vector<int>(t.size(),-1)); return process(s,s.size()-1,t,t.size()-1); } //递归含义str1[0.....i]范围上能搞定几个str2[0....j] int process(const string&str1,int i,const string&str2,int j) { if(i==0&&j==0){ return str1[i]==str2[j]?1:0; } if(i==0){//str1只有一个字符串但是str2不止一个字符串 return 0; } //str2只有一个字符串 if(j==0){ return str1[i]==str2[j]?process(str1,i-1,str2,j)+1:process(str1,i-1,str2,j); } if(dp[i][j]!=-1){ return dp[i][j]; } //可能性一:str1不要i位置的字符 int p1=process(str1,i-1,str2,j); //可能性二:str2要i位置的字符 int p2=str1[i]==str2[j]?process(str1,i-1,str2,j-1):0; //最终的结果为可能性一+可能性二 int ans =p1+p2; dp[i][j]=ans; return ans; } };
方法三:严格位置依赖的动态规划⭐⭐⭐⭐
class Solution { public: int numDistinct(string s, string t) { int M=s.size(); int N=t.size(); vector<vector<long long >>dp(M,vector<long long >(N)); dp[0][0]=s[0]==t[0]?1:0; for(int i=1;i<M;i++) { dp[i][0]=s[i]==t[0]?dp[i-1][0]+1:dp[i-1][0]; } for(int i=1;i<M;i++){ for(int j=1;j<N;j++){ dp[i][j]=dp[i-1][j]+(s[i]==t[j]?dp[i-1][j-1]:0); } } return dp[M-1][N-1]; } //递归含义str1[0.....i]范围上能搞定几个str2[0....j] int process(const string&str1,int i,const string&str2,int j) { if(i==0&&j==0){ return str1[i]==str2[j]?1:0; } if(i==0){//str1只有一个字符串但是str2不止一个字符串 return 0; } //str2只有一个字符串 if(j==0){ return str1[i]==str2[j]?process(str1,i-1,str2,j)+1:process(str1,i-1,str2,j); } //可能性一:str1不要i位置的字符 int p1=process(str1,i-1,str2,j); //可能性二:str2要i位置的字符 int p2=str1[i]==str2[j]?process(str1,i-1,str2,j-1):0; //最终的结果为可能性一+可能性二 int ans =p1+p2; return ans; } };