目录
题目一:回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
示例 1:
输入:s = "abc" 输出:3 解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa" 输出:6 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
这里需要注意的一点就是:子串必须是连续的,并且只要开始和结束位置不同,是可以出现相同的子串的
①状态表示
因为是需要枚举两次字符串才能得到所有的子串,所以我们可以创建一个二维的dp表,dp[i][j]
其中 i 表示行,j 表示列
i 小于等于 j
②状态转移方程
此时分为两大类,s[i]和s[j]是否相等:
如果两边的s[i]和s[j]都不同,那就返回false
如果相同,又细分为三种情况:重叠、相邻、不相邻
重叠和相邻直接返回true,如果不相邻,则需要继续看dp[i+1][j-1]的情况
③初始化
因为上述状态转移方程考虑到了,重叠和相邻这两种可能越界的情况,所以这里就不需要初始化了
④填表顺序
顺序是从下往上的,因为dp[i][j]用到了dp[i+1][j-1]的值
⑤返回值
所以返回值就是dp表中true的个数
代码如下:
class Solution
{
public:
int countSubstrings(string s)
{
int n = s.size(), ret = 0;
vector<vector<bool>> dp(n, vector<bool>(n));
for(int i = n - 1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
// 不需要处理不同的情况,dp表默认就是false
if(s[i] == s[j])
{
// 如果 i+1<j 说明 i 和 j 不重叠、不相邻,否则就是true
dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
// 统计结果
if(dp[i][j] == true) ret++;
}
}
}
return ret;
}
};
题目二:最长回文串
给你一个字符串 s
,找到 s
中最长的回文子串
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd" 输出:"bb"
最长回文子串,与上一题的思想基本相同,同样是创建一个二维的dp表,用于表示 i 位置开始 j 位置结束的子串是否是回文子串
所以这道题只需要在每个位置判断是否是回文子串,如果是则比较一下是否是当前最长的回文子串,得到其实位置和最大长度,就可以使用substr得到该子串了
代码如下:
class Solution
{
public:
string longestPalindrome(string s)
{
int n = s.size();
int len = 1, begin = 0;
vector<vector<bool>> dp(n, vector<bool>(n));
for(int i = n - 1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
if(dp[i][j] == true && j - i + 1 > len)
{
begin = i;
len = j - i + 1;
}
}
}
return s.substr(begin, ret);
}
};
题目三:回文串分割IV
给你一个字符串 s
,如果可以将它分割成三个 非空 回文子字符串,那么返回 true
,否则返回 false
。
当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串
示例 1:
输入:s = "abcbdd" 输出:true 解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。
示例 2:
输入:s = "bcbddxy" 输出:false 解释:s 没办法被分割成 3 个回文子字符串。
本题依旧是需要创建dp表,表示每个位置是否是回文子串
与之前的做法不同的是,本题要求将字符串分割为三个回文串,想要成功分割三个回文串,只需要分为三部分,[0, i - 1],[i, j],[j + 1, n - 1],只要存在这三部分都是回文串,就能够满足题目要求
所以我们只需要枚举第二个子串的开头结尾,就能够遍历上述所有情况,因为是枚举的第二个子串,所以不能从0下标开始,需要从1开始,且最后一个位置不能是n-1,最后一个位置是n-2,因为至少需要给第三个子串留一个位置
代码如下:
class Solution
{
public:
bool checkPartitioning(string s)
{
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n));
// 所有子串是否是回文进行预处理
for(int i = n - 1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
// 枚举所有第二个字符串的起始位置及结束位置
for(int i = 1; i < n - 1; i++)
for(int j = i; j < n - 1; j++)
if(dp[0][i-1] && dp[i][j] && dp[j+1][n-1])
return true;
return false;
}
};
题目四:回文串分割II
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文串
返回符合要求的 最少分割次数
示例 1:
输入:s = "aab" 输出:1 解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a" 输出:0
示例 3:
输入:s = "ab" 输出:1
①状态表示
②状态转移方程
dp[i]有两种情况:
0 ~ i 是回文串,dp[i] = 0
0 ~ i 不是回文串,在 0 < j ≤ i 的范围内,判断 j ~ i 是否是回文,如果是:回文串分割的次数就为dp[j - 1] + 1,所以 dp[i] = min(dp[j - 1] + 1, dp[i]);
③初始化
在计算 dp[i] = min(dp[j - 1] + 1, dp[i]) 时,为了不影响 min,所以将 dp 表内全部的值全部初始化为无穷大
④填表顺序
从左往右
⑤返回值
返回dp[n-1]
代码如下:
class Solution
{
public:
int minCut(string s)
{
int n = s.size();
// 先动归处理 i开头j结尾 的子串是否是回文
vector<vector<bool>> prev(n, vector<bool>(n));
for(int i = n - 1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
prev[i][j] = i + 1 < j ? prev[i+1][j-1] : true;
// 再动归处理 0 ~ i 位置的的子串最少需要分隔的次数
vector<int> dp(n, INT_MAX);
for(int i = 0; i < n; i++)
{
if(prev[0][i]) dp[i] = 0;
else
{
// 0~i 不是回文,就在 1 ~ i 的区间找是回文串的子串
for(int j = 1; j <= i; j++)
if(prev[j][i])
dp[i] = min(dp[j-1] + 1, dp[i]);
}
}
return dp[n-1];
}
};
动态规划:回文串问题 到此结束