0 这个问题是求一个串中,回文子串的最长的长度。
1 分析
首先分析是否能分解为子问题,s[0,i]与s[0,i+1]是否有关联?有关联,因为对s[0,i]后面加上一个字符x后,字符x可能是一个回文串的最后一个字符,从而造成回文串的增长。在串s[0,i]后面加上一个字符x后,挨个与之前的串进行比较,直到有字符s[a] == x;那么从s[a,i+i]这部分的回文串就可以进行延长。按照这种思路,我们需要用一个二维数组来进行记录两两下标之间的最大回文串长度。
2 递归式以及时间复杂度
用dp[i][j]来记录,从字符串s[i,j]的回文串的最长的值。
dp[i][j] = dp[i+1][j-1] + 2; if s[i] == s[j]
= max(dp[i+1][j], dp[i][j-1]) else
时间复杂度:o(n^2),空间复杂度:o(n^2)
时间复杂度解释,对二维数组进行穷举,需要覆盖全部的取值
空间复杂度解释,需要填充dp矩阵的三角形//代码在最后
3 优化
在时间上,问题没有优化空间,在空间上,有优化部分。为什么?因为我们看到递归式中dp[i][j]的求法,他只和局部的数据项有关,确切来说只有附近的三个值有关。进一步说,dp[i+1][j+1] = f(s[i],s[j], dp[i][j], dp[i+1][j], dp[i][j+1]) dp[i+1][j+1]是他附近上一项的函数。所以我们再这里可以进行优化为两个长度为n的数组进行表示。
4 代码
//空间复杂度为 o(n^2)
int longestPalindromeSubseq2(string s)
{
int n = s.size();
if (n <= 1)
return n;
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n - 1; ++i)
{
dp[i][i] = 1;
dp[i][i + 1] = 1 + (s[i] == s[i + 1]);
}
dp[n - 1][n - 1] = 1;
for (int i = 2; i < n; ++i)
for (int j = 0; j < n - i; ++j)
{
int row = j;
int col = i + j;
if (s[row] == s[col])
dp[row][col] = dp[row + 1][col - 1] + 2;
else
dp[row][col] = max(dp[row + 1][col], dp[row][col - 1]);
}
return dp[0][n - 1];
}
//空间复杂度为o(n)
int longestPalindromeSubseq(string s)
{
int n = s.size();
if (n <= 1)
return n;
vector<vector<int>> dp(n, vector<int>(2, 0));
for (int i = 0; i < n - 1; ++i)
{
dp[i][0] = 1;
dp[i][1] = 1 + (s[i] == s[i + 1]);
}
dp[n - 1][0] = 1;
for (int i = 2; i < n; ++i)
for (int j = 0; j < n - 1; ++j)
{
bool flag = i % 2;
int row = j;
int col = i + j;
if (s[row] == s[col])
dp[j][flag] = dp[j + 1][flag] + 2;
else
dp[j][flag] = max(dp[j][!flag], dp[j + 1][!flag]);
}
return max(dp[0][0], dp[0][1]);
}
5 代码分析
5.1 二维数组类似这个图,遍历时为对角遍历,这样可以不用递归求dp[i][j](直接用带memo的方法,可以写一个递归函数,一步求dp[0][n-1]也可以),遍历顺序为,黄色为i=0,灰色为i=1,橙色为i=2。我们再图中可以看到,在求dp[i][j]的时候,只用到了附近几个节点的数据,所以这个题目可以仅用两个数组来进行求解。另外,在对角遍历的时候,可以把遍历的方法记住,外层i循环的范围,内层j循环的范围,已经对应的row和col,在记住之后可以帮助提升编程效率。
5.2 在用数组的时候,可以创建两个dp来进行相互赋值,也可以利用上面的滚动数组进行方便书写量的减少。不过这都是实现细节,用自己熟悉的方法即可,主要是算法思想的掌握。