目录
简介
回文子串是算法刷题的高频考点,核心特征是:字符串中连续的子串,正读和反读完全相同(如 "aba"、"aa"、"a")。与学的 “子序列(非连续)” 不同,回文子串强调「连续」,解题思路也更聚焦于 “中心扩展”“动态规划” 等经典方法。
| 概念 | 定义 | 例子(s = "abcba") | 关键区别 |
|---|---|---|---|
| 回文子串 | 连续、正读 = 反读 | "a"(5 个)、"b"(2 个)、"c"、"bcb"、"abcba" → 共 7 个 | 连续 |
| 回文子序列 | 非连续、正读 = 反读 | "aba"、"aca"、"abcba" 等 → 远多于子串 | 非连续 |
这就有点像子数组和子序列
核心思想
定义状态
dp[i][j]:表示字符串s[i...j](从 i 到 j 的连续子串)是否为回文子串。
- 边界条件:
- 当
i == j(单个字符):dp[i][j] = true(所有单个字符都是回文);- 当
j - i == 1(两个字符):dp[i][j] = (s[i] == s[j]);- 转移方程:当
j - i > 1时,dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1](两端字符相等,且中间子串也是回文)。
注意:相比于动态规划,更优的算法是中心扩展算法和马拉车算法
马拉车只能解决回文子串、对于难题来说动态规划更好,动态规划能够将回文信息全部存入表中
关于面试,可以去了解一下中心扩展和马拉车,有时候面试官会要求优化
回文子串
比如这道题,我们是否可以枚举每个子串
当然,以i位置为起始,j位置为结束
则dp[i][j]:表示以i位置起始,j位置为结束的子串是否为回文子串
也就是我们需要填写一个二维dp表,并且dp表只要填写对角线和上三角就行


为什么说动态规划更适合解难题,因为你把回文子串的信息都保留在dp表当中
那这道题让你找到回文子串的个数,只需要返回表中为true的个数就行
初始化:无需初始化,因为不会越界
填表顺序:从下往上填写
返回值:true的个数
class Solution {
public:
int countSubstrings(string s) {
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
int ret = 0;
for (int i = m - 1; i >= 0; i--) {
for (int j = i; j < m; j++) {
if (s[i] == s[j]) {
if (i == j || i + 1 == j) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = false;
}
if (dp[i][j]) {
ret++;
}
}
}
return ret;
}
};
最长回文子串
因为有了第一题的铺垫,所以我们仅仅需要在dp表当中找到为true且为最长的即可
采用ret去记录长度,然后最长的时候记录begin,采用substr找到子串
s.substr(begin,ret);
class Solution {
public:
string longestPalindrome(string s) {
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
int ret = 0;
int begin;
for (int i = m - 1; i >= 0; i--) {
for (int j = i; j < m; j++) {
if (s[i] == s[j]) {
if (i == j || i + 1 == j) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
} else {
dp[i][j] = false;
}
if (dp[i][j]) {
if (ret < j - i + 1) {
ret = j - i + 1;
begin = i;
}
}
}
}
return s.substr(begin, ret);
}
};
分隔回文串
所以说为什么要学动态规划,因为这些题采用动态规划,可以把所有的回文信息填入dp表中
那你所有的题都是去dp表中寻找信息
那这道题就是分隔成三个回文串
在dp表当中判断这三个子串同时为回文串才行
枚举的时候只要想中间子串的起始和结束即可,也就能快速判断出i和j的范围
那中间子串前面肯定要有字符,则i起始肯定为1,j起始为i
那后面也要预留字符,则为m-2
class Solution {
public:
bool checkPartitioning(string s) {
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
int ret = false;
//填写dp表
for (int i = m - 1; i >= 0; i--) {
for (int j = i; j < m; j++) {
if (s[i] == s[j]) {
dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
}
}
}
//寻找结果[0,i-1][i,j][j+1,m-1]
for(int i=1;i<m-1;i++){
for(int j=i;j<m-1;j++){
if(dp[0][i-1]&&dp[i][j]&&dp[j+1][m-1]){
ret=true;
}
}
}
return ret;
}
};
分隔回文串Ⅱ
本题也是思考如何在原来的dp表信息当中寻找最优解
有点感觉在dp表的基础上还要一个动态规划
可以回看单词拆分,思想一样的
这个回文串优化点就会我们提前把回文信息存储到dp表当中
单词拆分存储到哈希表当中


class Solution {
public:
int minCut(string s) {
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
// 填写dp表
for (int i = m - 1; i >= 0; i--) {
for (int j = i; j < m; j++) {
if (s[i] == s[j]) {
dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
}
}
}
// 寻找最少分隔次数
vector<int> dp1(m, 0x3f3f3f3f);
for (int i = 0; i < m; i++) {
if (dp[0][i]) {
// 如果是回文串
dp1[i] = 0;
} else {
// 如果不是则往前遍历
for (int j = 1; j <= i; j++) {
if (dp[j][i]) {
dp1[i] = min(dp1[i], dp1[j - 1] + 1);
}
}
}
}
return dp1[m - 1];
}
};
最长回文子序列

以i位置为结尾推导的时候压根推不出来,因为你无法根据前面的状态推出dp[i]的状态
因为你不知道前面的子序列长啥样,你没有信息
那应该思考一下换别的状态,跟第一题一样,以区间[i,j]为状态,此时可以推出状态转移方程

核心就是:新加的字符s[i]和s[j]的关系
填表顺序:从下往上,从左往右
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (s[i] == s[j]) {
if (i == j)
dp[i][j] = 1;
else if (i + 1 == j)
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][n - 1];
}
};

关于状态表示,做了这么多道题,发现以区间[i,j]分析基本都是对的,尝试一下


初始化,因为i+1==j和i+1<j可以合并填写(画图发现,i+1==j用到的dp[i+1][j-1]为0),初始化的时候全部初始为0即可
然后会用到左边和下边的值,那填表的时候从左往右,从下往上即可
返回值:dp[0,m-1]
class Solution {
public:
int minInsertions(string s) {
int m = s.size();
vector<vector<int>> dp(m, vector<int>(m));
for (int i = m - 1; i >= 0; i--) {
for (int j = i; j < m; j++) {
// 从下往上,从左往右
if (s[i] == s[j]) {
if (i + 1 == j || i + 1 < j) {
dp[i][j]=dp[i+1][j-1];
}
}
else{
//不相等时取小
dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1;
}
}
}
return dp[0][m-1];
}
};
总结
回文类题型是字符串算法的核心考点,其中回文子串(连续) 和回文子序列(非连续) 是两类高频问题,解题思路既有共性(利用回文对称性),也有显著差异(连续 vs 非连续)。以下从「核心区别、通用解法、经典题型、避坑要点」四个维度做系统总结
| 维度 | 回文子串 | 回文子序列 |
|---|---|---|
| 连续性 | 必须连续(如 s="abcba" 的 "bcb") | 无需连续(如 s="abcba" 的 "aca") |
| 解题核心 | 利用「中心对称性」或「区间 DP」 | 仅能通过「区间 DP」(非连续无中心) |
| 时间复杂度 | 最优 O (n²)(中心扩展 / DP) | 最优 O (n²)(区间 DP) |
| 空间复杂度 | 中心扩展 O (1) / DP O (n²) | 区间 DP O (n²)(无法优化到 O (1)) |
| 核心特征 | 数量少(O (n²) 级) | 数量多(O (2ⁿ) 级,需 DP 避免枚举) |
- 回文子串(连续):
- 核心是「对称性」,最优解为中心扩展法(O (n²) 时间 + O (1) 空间);
- 分割类问题需先预处理回文 DP 表,再用 DP 求分割数。
- 回文子序列(非连续):
- 无中心可扩展,仅能通过区间 DP 解决(O (n²) 时间 + O (n²) 空间);
- 状态转移核心是「两端字符是否相等」,遍历顺序必须从短区间到长区间。
- 通用逻辑:
- 所有回文类问题都依赖「区间 DP」或「中心扩展」,暴力枚举(O (n³))仅用于理解概念;
- 预处理回文状态是进阶题的关键(如分割、计数),避免重复判断回文。
核心记住区间[i,j]的状态表示方式
dp表当中记录了回文子串的信息



19万+

被折叠的 条评论
为什么被折叠?



