云计算百科
云计算领域专业知识百科平台

【动态规划-2】5.最长回文子串

题目描述:

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串向前和向后读都相同,则它满足 回文性。

子字符串 是字符串中连续的 非空 字符序列。

 

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

解题思路:

方法一:中心扩展法(最优解)

核心思路:

回文串一定有一个中心:

  • 奇数长度:中心是一个字符(如 "aba" 中心是 b)

  • 偶数长度:中心是两个字符之间(如 "abba" 中心是 bb 之间)

从每个中心向两边扩展,找到最长的回文。

具体过程示例:

s = "babad"

中心 i=0 ('b'): 扩展 → "b"
中心 i=1 ('a'): 扩展 → "bab"
中心 i=2 ('b'): 扩展 → "aba"
中心 i=3 ('a'): 扩展 → "a"
中心 i=4 ('d'): 扩展 → "d"

偶数中心:
中心 i=0.5: 扩展 → ""
中心 i=1.5: 扩展 → ""
…

最长: "bab" 或 "aba" ✅

代码实现:

class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) return "";

int start = 0, maxLen = 1;

for (int i = 0; i < s.size(); i++) {
// 奇数长度回文,中心是 s[i]
int len1 = expandAroundCenter(s, i, i);
// 偶数长度回文,中心是 s[i] 和 s[i+1] 之间
int len2 = expandAroundCenter(s, i, i + 1);

int len = max(len1, len2);
if (len > maxLen) {
maxLen = len;
start = i – (len – 1) / 2;
}
}

return s.substr(start, maxLen);
}

private:
int expandAroundCenter(string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
left–;
right++;
}
return right – left – 1; // 回文长度
}
};

复杂度分析

维度复杂度说明
时间复杂度 O(n²) 每个中心扩展 O(n),共 n 个中心
空间复杂度 O(1) 只用常数个变量

方法二:动态规划

思路:

dp[i][j] 表示 s[i..j] 是否是回文。

  • s[i] == s[j] 且 dp[i+1][j-1] 为真 → dp[i][j] 为真

  • 边界:j – i <= 1 时,只要 s[i] == s[j] 就是回文

代码实现:

class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) return s;

vector<vector<bool>> dp(n, vector<bool>(n, false));
int start = 0, maxLen = 1;

// 初始化:单个字符都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}

// 按长度递增遍历
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len – 1 < n; i++) {
int j = i + len – 1;

if (s[i] == s[j]) {
if (len == 2 || dp[i+1][j-1]) {
dp[i][j] = true;
if (len > maxLen) {
maxLen = len;
start = i;
}
}
}
}
}

return s.substr(start, maxLen);
}
};

复杂度:时间 O(n²),空间 O(n²)

方法三:Manacher 算法

核心思路:

第一步:预处理,统一奇偶

回文有两种:

  • 奇数长度:"aba",中心是单个字符

  • 偶数长度:"abba",中心是两个字符之间

Manacher 的做法是插入特殊字符,把所有回文都变成奇数长度。

插入 #:

原串: a b a
新串: # a # b # a #

原串: a b b a
新串: # a # b # b # a #

效果:

  • 原来奇数长度 "aba"(长度3)→ 新串 "#a#b#a#"(长度7),中心是 b

  • 原来偶数长度 "abba"(长度4)→ 新串 "#a#b#b#a#"(长度9),中心是 #

所有回文都变成奇数长度,中心唯一。

再加两个哨兵:

新串: ^ # a # b # a # $

  • ^ 和 $ 是哨兵,防止扩展时越界

  • 它们不相等,扩展到这里一定停止

第二步:定义半径数组 p

p[i] 表示以 i 为中心的回文半径(包含中心)。

以 "#a#b#a#" 为例:

i字符p[i]回文
0 # 0 #
1 a 1 #a#
2 # 0 #
3 b 3 #a#b#a#
4 # 0 #
5 a 1 #a#
6 # 0 #

原串回文长度 = p[i](因为插入 # 后,半径正好等于原串回文长度)。

原串起始位置 = (i – p[i]) / 2。

第三步:核心——利用对称性

关键变量:

  • center:当前最右回文的中心

  • right:当前最右回文的右边界(center + p[center])

核心思想:

当遍历到 i 时,如果 i < right,说明 i 在某个回文内部。

利用对称性,i 关于 center 的对称点是:

mirror = 2 * center – i

此时 p[i] 至少等于 p[mirror],但有两种情况:

情况1: p[mirror] < right – i
→ p[i] = p[mirror](完全对称)

情况2: p[mirror] >= right – i
→ p[i] = right – i(只能确定这么多,需要继续扩展)

统一写法:

if (i < right) {
p[i] = min(right – i, p[mirror]);
}

图解:

center
↓
… [ … i … ] …
↑ ↑
mirror right

i 和 mirror 关于 center 对称

第四步:继续扩展

确定 p[i] 的下界后,继续向两边扩展:

while (t[i + p[i] + 1] == t[i – p[i] – 1]) {
p[i]++;
}

因为加了哨兵 ^ 和 $,不会越界。

第五步:更新 center 和 right

如果 i + p[i] > right,说明找到了更靠右的回文,更新:

if (i + p[i] > right) {
center = i;
right = i + p[i];
}

用例子走一遍:

s = "babad"

预处理:

t = "^#b#a#b#a#d#$"
下标: 0 1 2 3 4 5 6 7 8 9 10 11 12

遍历:

it[i]mirrorrightp[i]说明
1 # – 0 0 扩展失败
2 b – 0 1 #b#
3 # – 0 0 扩展失败
4 a – 0 3 #b#a#b#,center=4, right=7
5 # 3 7 0 i<right,p[5]=min(2, p[3]=0)=0
6 b 2 7 1 i<right,p[6]=min(1, p[2]=1)=1
7 # 1 7 0 i=right,扩展失败
8 a – 7 1 i>right,扩展 #a#
9 # – 7 0  
10 d – 7 1 #d#

maxLen = 3, maxCenter = 4

start = (4 – 3) / 2 = 0
结果 = s.substr(0, 3) = "bab" ✅

代码实现:

class Solution {
public:
string longestPalindrome(string s) {
// 预处理:插入 # 变成奇数长度
string t = "^#";
for (char c : s) {
t += c;
t += '#';
}
t += '$';

int n = t.size();
vector<int> p(n, 0);
int center = 0, right = 0;
int maxLen = 0, maxCenter = 0;

for (int i = 1; i < n – 1; i++) {
if (i < right) {
p[i] = min(right – i, p[2 * center – i]);
}

while (t[i + p[i] + 1] == t[i – p[i] – 1]) {
p[i]++;
}

if (i + p[i] > right) {
center = i;
right = i + p[i];
}

if (p[i] > maxLen) {
maxLen = p[i];
maxCenter = i;
}
}

int start = (maxCenter – maxLen) / 2;
return s.substr(start, maxLen);
}
};

复杂度分析:

维度复杂度说明
时间复杂度 O(n) 每个字符最多被扩展一次
空间复杂度 O(n) p 数组 + 预处理字符串

为什么是 O(n)?
因为 right 只增不减,每次扩展都会增加 right,总扩展次数不超过 n。

三种方法对比:

方法时间复杂度空间复杂度代码复杂度推荐度
中心扩展 O(n²) O(1) 简单 ⭐⭐⭐⭐⭐
动态规划 O(n²) O(n²) 中等 ⭐⭐⭐⭐
Manacher O(n) O(n) 复杂 ⭐⭐⭐

中心扩展是面试首选:代码简洁,空间 O(1),时间复杂度 O(n²) 对大多数场景够用。

关键细节:

1. 为什么中心扩展要处理奇偶两种情况?

  • 奇数回文:"aba",中心是单个字符

  • 偶数回文:"abba",中心是两个字符之间

所以需要对每个位置调用两次扩展:expand(i, i) 和 expand(i, i+1)。

2. 为什么返回 right – left – 1?

循环结束时,left 和 right 已经越界或不匹配。回文长度 = (right – 1) – (left + 1) + 1 = right – left – 1。

3. 动态规划的遍历顺序

必须按长度递增遍历,因为 dp[i][j] 依赖 dp[i+1][j-1](更短的子串)。

总结:

要点说明
核心思想 从每个中心向两边扩展
关键操作 奇数中心 (i,i),偶数中心 (i,i+1)
时间复杂度 O(n²)
空间复杂度 O(1)

 

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【动态规划-2】5.最长回文子串
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!