A.最长公共子串
很经典的动态规划问题,我们需要分析他的转移方程。
设f[i][j]表示两个字符串分别取以第i位作为结尾和第j位作为结尾的最长公共序列长度。我们可以这样分析:
- 当str1[i]=str[j]时,dp[i][j]=dp[i-1][j-1]+1
- 当str1[i]!=str[j]时,dp[i][j]=0
如此可以写出动态规划的代码:
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* longest common substring
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
string LCS(string str1, string str2) {
// write code here
vector<vector<int>> dp(str1.size()+1,vector<int>(str2.size()+1,0));
int res=0;
int pos=0;
for(int i=1;i<=str1.size();++i){
for(int j=1;j<=str2.size();++j){
//这里注意字符串的下标和dp数组的下标是不同的
if(str1[i-1]==str2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=0;
}
if(dp[i][j]>res){
res=dp[i][j];
pos=i-1;
}
}
}
return str1.substr(pos-res+1,res);
}
};
这里要注意的就是我们是自底向上进行优化的。以及在遍历的时候字符串用的是下标而动态规划数组用的是位置作为下标。
B.最长公共子序列
这题和上面那题的区别在于这个最长公共子序列是可以分割开的而不一定要是连续的。因此难度大一些。
这里贴上算法导论里的分析:
我们还是一样设置一个f[i][j]来动态规划,不过与上面那题不同的地方在于,这个i和j表示的不是以其作为结尾的相同子串,而是到i,j为止,记录的两个子串的最长公共序列。
状态转移方程如下:
在写代码的时候要注意一些细节,比如说下标的对应关系,以及我们在回溯最长序列的时候要先判断是否是从上面或者左边来的。如果先判断是否是左上方来的,可能会出现问题,可以这么想。
如果有一个点他的f值既大于左上方又等于上方,我们要往哪走?我们要往上走,因为从值上来看确实是有可能从左上方来,但是我们在走左上方的同时也会将对应的字符放到答案里面,但是从上面那张算法导论的图可以清晰地看出,唯有我们确定当前点不是从上面或左边得来的,才能确定这是公共序列中的元素,才能将其放入答案中。
class Solution {
public:
/**
* longest common subsequence
* @param s1 string字符串 the string
* @param s2 string字符串 the string
* @return string字符串
*/
string LCS(string s1, string s2) {
if(s1.empty() || s2.empty()) return "-1";
vector<vector<int>> dp(s1.size()+1,vector<int>(s2.size()+1,0));
for(int i=1;i<=s1.size();++i){
for(int j=1;j<=s2.size();++j){
dp[i][j]=(s1[i-1]==s2[j-1]) ? dp[i-1][j-1]+1 : max(dp[i-1][j],dp[i][j-1]);
}
}
string res;
for(int i=s1.size(),j=s2.size();dp[i][j]>=1;){
if(dp[i][j]==dp[i][j-1]){
j--;
}else if(dp[i][j]==dp[i-1][j]){
i--;
}else{
res+=s1[i-1];;
i--,j--;
}
}
reverse(res.begin(), res.end());
return res=="" ? "-1" : res;
}
};