文章目录
- 647.回文子串
- 思路
- CPP代码
- 双指针
- 516最长回文子序列
- 思路
- CPP代码
- 动态规划总结篇
647.回文子串
力扣题目链接
文章链接:647.回文子串
视频链接:动态规划,字符串性质决定了DP数组的定义 | LeetCode:647.回文子串
其实子串问题和子序列问题非常类似,也是存在最优子结构,那就意味着原问题的最优解可以由子问题的最优解推导出来。
其实回文的问题很容易联想到双指针。不过为了后续能够解决516.最长回文子序列问题,我们还是先使用动规解决一下该问题,为后面打基础。
思路
- 确定dp数组以及下标含义
我们回文子串的问题并不能像解决子序列问题那样去设置dp数组。
如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系,因为根本就无法跟相邻数组元素有什么关系。联想到使用双指针解决回文子串,我们的dp数组设置是不是也能参考设计呢?
最好的解决方法设置:布尔类型的dp[i][j]
:表示区间范围[i,j]
(注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]
为true,否则为false。
- 确定递推公式
两种情况:s[i]与s[j]相等,s[i]与s[j]不相等,其中相等时又分为三种情况
如果不相等,那dp[i][j]=false
如果相等:
- 下标i 与 j相同,同一个字符例如a,当然是回文子串
- 下标i 与 j相差为1,例如aa,也是回文子串
- 下标:
i
与j
相差大于1的时候,例如cabac
,此时s[i]
与s[j]
已经相同了,我们看i
到j
区间是不是回文子串就看aba
是不是回文就可以了,那么aba
的区间就是i+1
与j-1
区间,这个区间是不是回文就看dp[i + 1][j - 1]
是否为true。
综上所述:
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
- 如何初始化
全是设置为false
- 确定遍历顺序
二维的递推顺序最好就是根据递推公式画一个格子:
![在这里插入图片描述](
我们需要首先知道左下角的格子,才能推导出我们的dp,
所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的。
- 推导dp数组
输入:“aaa”,dp[i][j]
状态如下:
图片中有6个true,所以就有六个回文子串
CPP代码
//完整代码
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
};
//简洁版代码
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
result++;
dp[i][j] = true;
}
}
}
return result;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
双指针
回文必须还得是双指针
class Solution {
public:
int countSubstrings(string s) {
int count = 0;
for (int i = 0; i < s.length(); ++i) {
count += countPalindrome(s, i, i); // 以 s[i] 为中心的回文子串数量
count += countPalindrome(s, i, i + 1); // 以 s[i] 和 s[i+1] 为中心的回文子串数量
}
return count;
}
private:
int countPalindrome(const string& s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
++count;
--left;
++right;
}
return count;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
516最长回文子序列
力扣题目链接
文章链接:516最长回文子序列
视频链接:动态规划再显神通,LeetCode:516.最长回文子序列
对于这个问题,使用双指针并不是最有效的方法。因为最长回文子序列不要求是连续的字符,而是可以跳过某些字符形成回文。本题中,回文子序列与回文子串是有区别的。
思路
- 确定dp数组以及下标的含义
dp[i][j]
:字符串s
在[i, j]
范围内最长的回文子序列的长度为dp[i][j]
- 确定递推公式
在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。
如果s[i]
与s[j]
相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
具体如图所示:
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
加入s[j]的回文子序列长度为dp[i + 1][j]
。
加入s[i]的回文子序列长度为dp[i][j - 1]
。
那么dp[i][j]
一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
- dp数组如何初始化
从递推公式可以看出,递推公式计算不到i 和j相同时候的情况。
当i与j相同,那么dp[i][j]
一定是等于1的,即:一个字符的回文子序列长度就是1。
其他都应该初始化成0,这样题对公式dp[i][j]=max(dp[i + 1][j], dp[i][j - 1]);
才不会被初始值覆盖
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
- 确定遍历顺序
从递归公式中,可以看出,dp[i][j]
依赖于 dp[i + 1][j - 1]
,dp[i + 1][j]
和 dp[i][j - 1]
,如图:
所以当我们遍历i的时候是从上到下,然后j是从左到右
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); j++) {
...
}
}
- 举例推导dp数组
输入s:“cbbd” 为例,dp数组状态如图:
红色框即:dp[0][s.size() - 1]
; 为最终结果。
CPP代码
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.size() - 1];
}
};
动态规划总结篇
二刷回来整总结!