目录:
解题及思路学习
647. 回文子串
https://leetcode.cn/problems/palindromic-substrings/
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
思考:要连续的子串,并统计回文的个数。之前学过的回溯算法应该能解。
如果按照动态规划的思路来。
随想录:dp数组的定义,如果一个区间是回文子串的话,那往两边各取一个数,如果相同,那就是一个新的回文子串。
1、含义
根据回文的定义,布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4i19JB5v-1689737666160)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e71dd2de-1725-4c72-b208-446d6e89b69e/Untitled.png)]
2、递推公式
整体上可以分为s[i] 和 s[j] 是否相等两种情况。
当s[i] 和 s[j] 相等时:
① 下标相同,例如一个字符,自然是回文子串。
② 下标相差1,例如aa,也是回文子串。
③ 下标相差大于1,例如cabac,此时s[i]与s[j] 相同,区间 [i + 1, j - 1] 也是回文子串,所以是。
当s[i] 和s[j] 不相等的时候,自然就不是回文子串。
3、初始化
dp[i][j] 全部初始化为false
4、遍历顺序
因为dp[i][j] 依赖左下角的的结果,所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-81KrNtKc-1689737666162)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/21b0a1bb-d85c-43f0-9ef2-8dace0fb2900/Untitled.png)]
5、举例推导dp数组
举例,输入:“aaa”,dp[i][j]状态如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFeCAPuO-1689737666164)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e2fd9506-2085-4ebe-b125-45879a9a530c/Untitled.png)]
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;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
双指针法
首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。
在遍历中心点的时候,要注意中心点有两种情况。
一个元素可以作为中心点,两个元素也可以作为中心点。
那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。
所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。
class Solution {
public:
int countSubstrings(string s) {
int result = 0;
for (int i = 0; i < s.size(); i++) {
result += extend(s, i, i, s.size()); // 以i为中心
result += extend(s, i, i + 1, s.size()); // 以i和i+1为中心
}
return result;
}
int extend(const string& s, int i, int j, int n) {
int res = 0;
while (i >= 0 && j < n && s[i] == s[j]) {
i--;
j++;
res++;
}
return res;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
双指针解法也挺巧妙的。
516.最长回文子序列
https://leetcode.cn/problems/longest-palindromic-subsequence/
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
思考:可以仿照上题的思路,对于[i, j] 区间内的最长子序列表示为dp[i][j],如果s[i] == s[j] ,那么最长长度就等于[i+1, j-1] + 2, 否则,只考虑一端的最大值。
1、含义
dp[i][j] 表示区间[i,j] 内s的最长的回文子序列长度。
2、递推公式
当s[i] 和s[j] 相等时:
① i和j相等,那么dp[i][j] = 1;
② i和j相差1,那么dp[i][j] = 2;
③ i和j相差大于1, 那么dp[i][j] = dp[i+1][j - 1] + 2;
当s[i] 和s[j] 不相等时:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 即,不带边界的最大长度。
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (i == j) dp[i][j] = 1;
else if (j - i == 1) dp[i][j] = 2;
else
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];
}
};
- 时间复杂度: O(n^2)
- 空间复杂度: O(n^2)
随想录:基本上思想一样的,只是在代码实现上略有差别。对于i和j相等的情况,分别单独赋值为1。然后在遍历j的时候从 i+1开始遍历。
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];
}
};
- 时间复杂度: O(n^2)
- 空间复杂度: O(n^2)
动态规划总结篇
https://programmercarl.com/动态规划总结篇.html
知识点记录
知识点
回文子串
个人反思
动态规划类题目,最主要的是递推公式。定义的好可以省事儿很多,其他细节也很重要。
动规五部曲分别为:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
按照这个来分析挺好的