_49LeetCode代码随想录算法训练营第四十九天-动态规划 | 647.回文子串、516.最长回文子序列
题目列表
- 647.回文子串
- 516.最长回文子序列
- 动态规划总结篇
647.回文子串
代码随想录地址:https://programmercarl.com/0647.%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2.html
题目
给你一个字符串
s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
-
1 <= s.length <= 1000
-
由小写英文字母组成s
思路
动态规划思路
个人认为不好理解。
咬死了只有两种基础情况,也就是i和j指向同一个元素,第二个是i和j只相差1,并且s[i] == s[j]。
动规五部曲:
- 确定dp数组(dp table)以及下标的含义
观察回文串的性质,如下图:

如果 s[1],s[2],s[3] 是回文的,只需要比较s[0]和s[4]元素是否相同,相同的话字符串S就是回文。
找到一种递归关系,判断一个子字符串(字符串的下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。
将dp数组是要定义为二维dp数组。
布尔类型的dp[i] [j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i] [j]为true,否则为false。
- 确定递推公式
当s[i]与s[j]不相等,dp[i] [j]一定是false。
当s[i]与s[j]相等时,有三种情况
- 情况一:下标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;
}
}
result就是统计回文子串的数量。
此处没有考虑s[i]与s[j]不相等的时候,因为在初始化dp[i] [j]的时候,就初始为false。
- dp数组如何初始化
所以dp[i] [j]初始化为false。
- 确定遍历顺序
遍历顺序根据递推公式决定,由于dp[i] [j]与dp[i + 1] [j - 1]相关,因此遍历顺序必须是从下到上、从左到右。
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;
}
}
}
}
- 举例推导dp数组
草稿纸。
双指针思路
中心点向外扩散的思想:
在遍历中心点的时候,要注意中心点有两种情况。
一个元素可以作为中心点,两个元素也可以作为中心点。
三个元素是由一个元素为中心点扩散而来;四个元素为中心点是由两个元素为中心点扩散而来。
代码
动态规划代码
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
/*
* @lc app=leetcode.cn id=647 lang=cpp
*
* [647] 回文子串
*/
// @lc code=start
class Solution {
public:
int countSubstrings(string s) {
int res = 0;//记录回文子串的个数
//定义和初始化dp数组
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
//遍历
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)
{
res++;
dp[i][j] = true;
}
else if(dp[i + 1][j - 1])
{
res++;
dp[i][j] = true;
}
}
}
}
return res;
}
};
// @lc code=end
双指针思路
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
/*
* @lc app=leetcode.cn id=647 lang=cpp
*
* [647] 回文子串
*/
// @lc code=start
class Solution {
private:
int extend(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;
}
public:
int countSubstrings(string s) {
int res = 0;
for(int i = 0; i < s.size(); i++)
{
res += extend(s, i, i, s.size());//以一个点为中心
res += extend(s, i, i + 1, s.size());//以两个点为中心
}
return res;
}
};
// @lc code=end
5.最长回文子串
题目
给你一个字符串
s
,找到
s
中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
-
1 <= s.length <= 1000
-
仅由数字和英文字母组成s
思路
双指针的思路,和 647.回文子串 的双指针思路有点像。
代码
/*
* @lc app=leetcode.cn id=5 lang=cpp
*
* [5] 最长回文子串
*/
// @lc code=start
class Solution {
private:
//返回最长回文子串
int extend(string& s, int i, int j, int n)
{
int ex = 0;
while(i >= 0 && j < n && s[i] == s[j])
{
i--;
j++;
ex++;
}
return ex;
}
public:
string longestPalindrome(string s) {
//记录最长回文子串的起始位置和终止位置,是前闭后闭区间
int begin = 0;
int end = 0;
for(int i = 0; i < s.size(); i++)
{
//一个中心节点开始扩散
int temp = extend(s, i, i, s.size());
if(temp * 2 - 1 > end - begin + 1)
{
begin = i - (temp - 1);
end = i + (temp - 1);
}
//两个中心节点开始扩散
temp = extend(s, i, i + 1, s.size());
if(temp * 2 > end - begin + 1)
{
begin = i - temp + 1;
end = i + temp;
}
}
return string(s.begin() + begin, s.begin() + end + 1);
}
};
// @lc code=end
516.最长回文子序列
代码随想录地址:https://programmercarl.com/0516.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E5%BA%8F%E5%88%97.html
这个题目说明初始化很重要。
题目
给你一个字符串
s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
提示:
-
1 <= s.length <= 1000
-
仅由小写英文字母组成s
思路
- 确定dp数组(dp table)以及下标的含义
dp[i] [j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i] [j]。
- 确定递推公式
s[i] == s[j],那么dp[i] [j] = dp[i + 1] [j - 1] + 2;
如图:
s[i] != s[j]:可以加入s[i]不加入s[j],也可以加入s[j]不加入s[i],看哪个更大。
dp[i] [j] = max(dp[i + 1] [j], dp[i] [j - 1]);
- dp数组如何初始化
首先要考虑当i 和j 相同的情况,从递推公式:dp[i] [j] = dp[i + 1] [j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。
所以需要手动初始化一下,当i与j相同,那么dp[i] [j]一定是等于1的,即:一个字符的回文子序列长度就是1。
其他情况dp[i][j]初始为0就行,这样递推公式中dp[i] [j]才不会被初始值覆盖。
- 确定遍历顺序
根据递推公式,从下到上,从左到右。
- 举例推导dp数组
草稿纸。
代码
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
/*
* @lc app=leetcode.cn id=516 lang=cpp
*
* [516] 最长回文子序列
*/
// @lc code=start
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;
//遍历
//j一定要在i的右侧
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][j - 1], dp[i + 1][j]);
}
return dp[0][s.size() - 1];
}
};
// @lc code=end
动态规划总结篇
代码随想录地址:https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%80%BB%E7%BB%93%E7%AF%87.html