本周继续练习动态规划的相关题目。记录两道印象深刻的题,分别是05最长回文子串和115不同的子序列。
Longest Palindromic Substring最长回文子串
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
给了字符串s,找到s中最长的回文子串,s最长是1000.
(注意子串是连续的,与子序列不一样)
思路一:暴力破解(TLE)
定义一个judge函数判断序列是否为回文序列。使用两个循环变量i和j,外层循环变量为i,内层循环j从i+1开始遍历给定的字符串s,每次增加一个字符,都进入judge函数判断是否为回文序列,如果是,与之前已记录的最长序列res比较,将长的一个赋值给res。
很naive的做法,子串要遍历两次,每个还要进入judge函数判断,结果明显是超时的。
思路二:暴力破解的改进(436ms)
同样使用两个循环变量i和j,外层循环变量为i,内层循环j从i+1开始遍历给定的字符串s,每次增加一个字符。修改的步骤如下:
1)每次判断如果字符串的第一个字母和新添加的字母相同,那么进入一个judge函数判断是否为回文串,如果是,每次都比较取最长的一个;
2)在j遍历一次完整个字符串后,判断如果得到的回文串已经读取到字符串s的最后一个字符,那么,直接跳出循环,得到的结果就是最长的(因为已经读到最后了)。
代码
bool judge(string s){
for(int i = 0; i < s.length() /2; i ++)
if(s[i] != s[s.length()-1-i])
return false;
return true;
}
string longestPalindrome(string s) {
string temp;
int lastindex = 0;
string res = s.substr(0,1);
if(s.length() == 1)
return s;
for(int i = 0; i < s.length(); i ++)
{
temp += s[i];
for(int j = i+1; j < s.length(); j ++){
temp += s[j];
if(temp[0] == s[j] && judge(temp)){
res = res.length() < temp.length() ? temp : res;
lastindex = j;
}
}
if(lastindex == s.length()-1)
break;
temp = "";
}
return res;
}
思路三:动态规划算法(86ms)
使用DP方法,定义一个bool数组p,子问题p[i][j]表示从字符串s的第i位到第j位是否为回文串;因此i < j的部分不需要用到。
初始状态:
当i = j时,p[i][j] = 1;
当j = i + 1时,p[i][j] = (s[i] == s[j]);
转移方程:外循环变量i从n-3开始,内循环j从i+2开始,则
p[i][j] = (p[i+1][j-1]) && (s[i] == s[j]);
如果p[i][j]是1,那么比较上一次记录的最长回文子序列的长度和当前回文子序列的长度,记下长的一个。
我们举个例子,假设一个字符串长度是6,s = ababba那么初始化时,得到一下矩阵。
因为p[i][j]是根据p[i+1][j-1]推得,要得到p[i][j]首先要知道它的左下角是什么。如下图所示,所以要从倒数第三行开始往上计算。
代码(DP)
string longestPalindrome(string s) {
int n = s.length();
if(n <= 1)
return s;
bool p[n][n];//p[i][j]表示从s[i]到s[j]是否为回文串
memset(p,0,sizeof(p));
string res = s.substr(0, 1);
for(int i = 0; i < n; i ++){
p[i][i] = true;
if(i+1<n){
p[i][i+1] = (s[i] == s[i+1]);
if(p[i][i+1])
res = s.substr(i, 2);
}
}
for(int i = n-3; i >= 0; i --){
for(int j = i+2; j < n; j ++){
p[i][j] = (p[i+1][j-1]) && (s[i] == s[j]);
if(p[i][j])
{
if(j-i+1 >= res.length()){
res = s.substr(i, j-i+1);
}
}
}
}
return res;
}
Distinct Subsequences不同的子串
给了两个字符串s和t,计算s中有多少个不同的子串t。这里的子串指的是一个由原字符串删去或者不删某些字符,不改变剩余字符顺序所得到的的字符串。举个例子,”ACE”是字符串”ABCDE”的子串,而”AEC”不是。
题目中如果s是“rabbbit”,t是“rabbit”,那么子串的数量是3个。
思路:
一开始想的就是用动态规划的思路,定义dp[i][j]表示字符串t的前i个字符和字符串s的前j个字符的匹配的子串数。dp数组最好是定义(tlen+1) * (slen+1)这么大,其中tlen表示字符串t的长度,slen表示字符串s的长度。
初始化状态时:
1)dp[i][0]表示字符串s取0个字符,字符串t取i个字符,当i不为0时,dp[i][0]值必然是0,若i 和j都为0,表示两个字符串都是空,那么dp[0][0]就是1,即s中有1个子串t;
2)dp[0][j]表示字符串t取0个字符,字符串s取j个字符,此时dp[i][0]值必然是1;因为空串是任何字符串的子串;
如何定义状态转移方程?
考虑一个字符串t在字符串s中的子串,可以先找t的第一个字符在s中出现的位置,接着t的第二个字符,必须是在第一个字符出现过的后面。举个例子,
我们现在先假设字符串是从1开始,考虑得到dp[i][j]的子问题,如果在该位置有t[i]等于s[j],那么要注意的是它的左上角,即(i-1, j-1)的位置,表示上一个匹配的位置。(不是考虑正上方(i-1, j)的位置,因为这个位置表示的是的第i-1行到第j列的字符串情况,而当前第i行正在第j列,考虑的只能是j-1列的情况,不然会出现子串顺序出问题)
a
b
a
b
g
b
a
g
a
g
b
1
2
3
a
√
√
√
g
说回t[i]等于s[j]时,dp[i-1][ j-1]的子串数就是在t[i]等于s[j]时dp[i][ j]的一部分,另一部分是dp[i][j-1],它表示字符串t在i位置时,与前面j-1列的匹配情况,这一部分肯定也是属于dp[i][j]的。所以,转移方程如下所示:
- 当t[i] 不等于s[j]时,dp[i][j] = dp[i][j-1];
- 当t[i] 等于s[j]时,dp[i][j] = dp[i][j-1] + dp[i-1][j-1];
那么上面的矩阵更新可以如下所示:
如果字符串是从0开始,那么转移方程就是:
a
b
a
b
g
b
a
g
a
g
b
0
1
1
2
2
3
3
3
3
3
a
0
0
1
1
1
1
4
4
7
7
g
0
0
0
0
1
1
1
5
5
12
- 当t[i-1] 不等于 s[j-1] 时,dp[i][j] = dp[i][j-1];
- 当t[i-1] 等于 s[j-1] 时,dp[i][j] = dp[i][j-1] + dp[i-1][j-1];
代码
int numDistinct(string s, string t) {
int tlen = t.length(), slen = s.length();
int dp[tlen+1][slen+1];
memset(dp, 0, sizeof(dp));
for (int j = 0; j <= slen; j++)
dp[0][j] = 1;
for (int i = 1; i <= tlen; i++)
for (int j = 1; j <= slen; j++){
if(s[j-1] == t[i-1])
dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1];
else
dp[i][j] = dp[i][j - 1];
}
return dp[tlen][slen];
}
总结
这周做题目感觉比之前要顺手了,虽然有时候不能直接想到最好的思路,但是通过一次次改进还是能够完善好,至少是有点进步的 吧.....