#LC5. 最长回文子串
最长回文子串
你好!我是你的OI竞赛教练。很高兴看到你开始挑战经典的字符串题目。LeetCode5这道题虽然在LeetCode上是中等难度,但在OI体系中,它是区间动态规划(Interval DP)和中心扩展法的入门必修课,甚至通向更高阶的Manacher算法。
下面我将把这道题改写成标准的NOI竞赛题目格式,并带你一步步拆解思路。
一、 题目描述
题目名称: 最长回文子串 (Longest Palindromic Substring)
输入文件: palindrome.in
输出文件: palindrome.out
时间限制: 1.0 s
空间限制: 256 MB
【题目描述】
给定一个字符串 ,请你找出 中最长的回文子串。 如果 中存在多个长度相同的最长回文子串,输出其中任意一个即可。
定义:
- 子串 (Substring):字符串中连续的字符序列。
- 回文 (Palindrome):正着读和反着读都一样的字符串。例如 "aba" 是回文,而 "abc" 不是。
【输入格式】
输入包含一行,为一个字符串 。 仅由数字 (0-9) 和英文字母 (a-z, A-Z) 组成。
【输出格式】
输出一行,包含一个字符串,表示 的最长回文子串。
【样例 1】
输入:
babad
输出:
bab
说明: "aba" 也是一个有效答案。
【样例 2】
输入:
cbbd
输出:
bb
【数据范围与提示】
对于 的数据,字符串 的长度 。
二、 预备知识点总结
在攻克这道题之前,你需要掌握以下知识点:
- 字符串基础:了解字符串的下标操作(C++中通常从0开始)。
- 回文判定:如何用双指针法判断一个字符串是否是回文。
- 枚举思想:如何遍历所有可能的子串。
- 动态规划 (DP):特别是区间DP的基础概念(状态定义与转移)。
- 中心扩展法:一种利用回文对称性来优化枚举的技巧。
三、 启发式教学:思路推理与草稿纸推演
同学们,拿出你们的草稿纸,我们不要一上来就写代码,先画图理解。
1. 朴素思路(暴力法)—— 为什么会超时?
- 提问:如果我们要找到最长的回文子串,最笨的办法是什么?
- 学生思考:枚举所有的子串,然后检查它是不是回文。
- 草稿纸推演:
假设字符串是
babad。- 子串有
b,ba,bab,baba,babad,a,ab, ... - 子串总数是 级别的。
- 检查每一个子串是否回文需要 的时间。
- 总复杂度:。
- 子串有
- 教练点评:数据范围 , ,这在1秒的时限内大概率会超时(通常计算机1秒处理 次运算)。我们需要优化。
2. 优化视角一:中心扩展法(形象直观)
- 引导:回文串有什么特点?它是对称的。既然是对称的,如果我们知道了“中心”,是不是就可以向两边扩散去寻找边界?
- 画图:
在草稿纸上写下
b a b a d。- 假设中心是
b(下标2),向左看是a,向右看是a。一样!继续扩散。 - 再向左是
b,向右是d。不一样!停止。 - 找到了
abc...哦不对,是aba。
- 假设中心是
- 难点提示:中心只有字符吗?
看样例
c b b d。最长回文是bb。- 如果中心只选字符,选第一个
b,左右不匹配;选第二个b,左右不匹配。 - 关键点:回文的中心可能是一个字符(长度为奇数),也可能是两个字符之间的空隙(长度为偶数)。
- 如果中心只选字符,选第一个
- 算法流程:
- 枚举每一个可能的中心(共有 个字符中心 + 个空隙中心 = 个中心)。
- 对于每个中心,向两边扩展,直到字符不相等。
- 记录最长的长度和起始位置。
- 复杂度分析:
- 时间:枚举中心 扩展 = 。对于 , 次运算,绰绰有余。
- 空间:,只需要存几个变量。
3. 优化视角二:动态规划 (DP) —— 经典的区间DP
- 引导:如果不从中心开始,我们用数学归纳法的思想。如果我们知道
bab是回文,那么ababa是不是回文取决于什么? - 推导:
- 取决于两头的字符是否相等 (
a==a) 且 中间的部分 (bab) 是否已经是回文。
- 取决于两头的字符是否相等 (
- 状态定义: 令 表示子串 是否为回文串(True/False)。
- 状态转移方程:
边界条件:
- 长度为1:
- 长度为2:
- 填表顺序:
同学们注意!在草稿纸上画一个二维表格。如果我们要算 ,我们需要先知道 。
所以,不能简单地双层
for (i=0..n) for (j=0..n)。 必须按子串长度(len)从小到大枚举! 先算长度为1的,再算长度为2的... - 复杂度分析:
- 时间:(填满半个表格)。
- 空间:(需要一个二维数组)。
4. 教练建议
对于 ,中心扩展法在空间上优于DP,且代码更容易写对(不需要考虑DP的遍历顺序)。DP是很好的思维训练,但在这题上略显笨重。
进阶思考(课后作业):如果 ,怎么做?去查阅 Manacher算法 (马拉车算法),它可以做到 。
四、 读题关键词 (Reading Comprehension)
做题时圈出这些词,你就赢了一半:
- “连续子串” (Substring):不是子序列,必须连续。
- “最长”:需要维护一个
max_len变量,并在发现更长时更新start_index。 - “任意一个”:不需要纠结输出第一个还是最后一个,遇到一样长的,不更新或者更新都可以。
五、 参考代码 (NOI风格 C++14)
这里提供中心扩展法的代码,这是考场上性价比最高的写法。
/**
* Title: Longest Palindromic Substring
* Language: C++14 (NOI Standard)
* Time Complexity: O(N^2)
* Space Complexity: O(1) (excluding input/output storage)
*/
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
// 辅助函数:从中心向两边扩展,返回该中心能构成的最长回文串长度
// left 和 right 是扩展的起始位置
int expandAroundCenter(const string& s, int left, int right) {
int L = left, R = right;
// 当没有越界,且两端字符相等时,继续扩展
while (L >= 0 && R < s.length() && s[L] == s[R]) {
L--;
R++;
}
// 注意:跳出循环时,s[L] != s[R] 或者越界
// 回文串的真实范围是 (L + 1) 到 (R - 1)
// 长度 = (R - 1) - (L + 1) + 1 = R - L - 1
return R - L - 1;
}
int main() {
// 优化 I/O 效率,这是OI选手的基本素养
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
if (!(cin >> s)) return 0; // 处理空输入等异常情况
if (s.empty()) {
cout << "" << endl;
return 0;
}
int start = 0, maxLen = 0;
for (int i = 0; i < s.length(); ++i) {
// 情况1:回文中心是一个字符(如 "aba" 中心是 'b')
int len1 = expandAroundCenter(s, i, i);
// 情况2:回文中心是两个字符之间(如 "abba" 中心是 "bb" 之间)
int len2 = expandAroundCenter(s, i, i + 1);
// 取两种情况的较大值
int len = max(len1, len2);
// 如果找到了更长的回文串,更新起始位置和最大长度
if (len > maxLen) {
maxLen = len;
// 推导起始位置:
// 若 len 是奇数,中心是 i,start = i - len/2
// 若 len 是偶数,中心是 i 和 i+1 之间,start = i - (len-1)/2
// 整数除法特性:(len - 1) / 2 等价于统一公式
start = i - (len - 1) / 2;
}
}
// 输出最长回文子串
cout << s.substr(start, maxLen) << endl;
return 0;
}
六、 课后自我检查
- 代码中
R - L - 1是怎么推导出来的?(建议带入aba和abba算一下边界) - 如果不使用
start和maxLen记录,而是每次都截取substr比较,会增加多少时间复杂度?(字符串拷贝是 的,会导致总复杂度退化为 ,千万小心)。
祝你切题愉快!