文章目录
- 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
- 确定遍历顺序
二维的递推顺序最好就是根据递推公式画一个格子:
 {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];}
};
动态规划总结篇
二刷回来整总结!