题解:洛谷 P3805 Manacher
【题目来源】
【题目描述】
给出一个只由小写英文字符 \(\texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z\) 组成的字符串 \(S\) ,求 \(S\) 中最长回文串的长度 。
字符串长度为 \(n\)。
【输入】
一行小写英文字符 \(\texttt a,\texttt b,\texttt c,\cdots,\texttt y,\texttt z\) 组成的字符串 \(S\)。
【输出】
一个整数表示答案。
【输入样例】
aaa
【输出样例】
3
【核心思想】
-
问题分析:给定长度为 \(n\) 的字符串 \(S\)(仅含小写字母),求 \(S\) 中最长回文子串的长度。这是一个Manacher 算法模板题,关键在于利用回文串的对称性,将暴力中心扩展的 \(O(n^2)\) 优化到线性 \(O(n)\)。
-
算法选择:
- 字符间隔插入:在相邻字符间及首尾插入分隔符
#,将奇偶长度回文统一为奇数长度回文处理 - Manacher 算法:维护当前最右回文串的边界 \([l, r]\),对每个中心位置 \(i\) 利用对称位置 \(d[r-i+l]\) 初始化回文半径,减少重复扩展
- 字符间隔插入:在相邻字符间及首尾插入分隔符
-
关键步骤:
- 预处理:原字符串 \(a[1..n]\),构造新字符串 \(s\):
$#a_1#a_2#...#a_n#,长度变为 \(2n+1\) - 计算回文半径 \(d[i]\)(\(i\) 从 \(2\) 到 \(n\)):
- 对称初始化:若 \(i \leq r\),则 \(d[i] = \min(d[r-i+l], r-i+1)\)(利用已计算的对称位置,但不能超过右边界)
- 中心扩展:
while (s[i-d[i]] == s[i+d[i]]) d[i]++ - 更新边界:若 \(i+d[i]-1 > r\),则 \(l = i-d[i]+1, r = i+d[i]-1\)
- 统计答案:\(\max(d[i]) - 1\)(\(d[i]\) 包含分隔符,实际回文长度为 \(d[i]-1\))
- 预处理:原字符串 \(a[1..n]\),构造新字符串 \(s\):
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个位置最多被扩展一次,均摊线性
- 空间复杂度:\(O(n)\),存储处理后的字符串和回文半径数组
-
Manacher 算法的核心思想:
- 统一奇偶:插入
#后,原字符串的任意回文子串都对应新字符串中以某个#或原字符为中心的奇数长度回文,无需分类讨论 - 对称性复用:若当前中心 \(i\) 在最右回文串 \([l, r]\) 内部,则 \(i\) 关于中心的对称点 \(j = r-i+l\) 的回文半径已知,可直接作为 \(d[i]\) 的下界,避免从头扩展
- 边界限制:\(d[i]\) 的初始值受限于 \(i\) 到右边界 \(r\) 的距离,防止越界并保证正确性
- 最右边界维护:每次扩展后更新 \([l, r]\),确保后续位置能尽可能利用对称信息
- 适用于"最长回文子串"及其变种问题,是线性时间求解回文结构的经典算法
- 统一奇偶:插入
【算法标签】
提高 #Manacher
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 11000005 * 2; // 定义数组最大容量(原字符串长度*2+5,处理后的字符串长度)
char a[N], s[N]; // a存储原始字符串,s存储处理后的字符串(插入分隔符#)
int d[N], ans; // d[i]为Manacher算法中以i为中心的最长回文半径,ans记录最长回文串长度
// Manacher算法核心函数:计算以每个位置为中心的最长回文半径
// s为处理后的字符串(已插入#),n为处理后字符串的长度
void get_d(char *s, int n)
{
d[1] = 1; // 初始化:以第一个字符为中心的回文半径为1(只有自身)
// i遍历每个中心位置,l和r维护当前最右回文串的左右边界
for (int i = 2, l, r = 1; i <= n; i++)
{
// 如果当前位置i在当前最右回文串[r]的范围内,利用对称性初始化d[i]
// d[r-i+l]是i关于中心l的对称位置的回文半径,r-i+1是i到右边界r的距离
// 取两者较小值作为d[i]的初始值(不能超过右边界)
if (i <= r)
d[i] = min(d[r - i + l], r - i + 1);
// 中心扩展:尝试向两边扩展回文串
while (s[i - d[i]] == s[i + d[i]])
d[i]++; // 两边字符相同,回文半径加1
// 更新最右回文串边界:如果当前回文串右边界超出r
if (i + d[i] - 1 > r)
l = i - d[i] + 1, r = i + d[i] - 1; // 更新左边界l和右边界r
}
}
int main()
{
scanf("%s", a + 1); // 读入原始字符串(从a[1]开始存储)
int n = strlen(a + 1), k = 0; // n为原始字符串长度,k为处理后字符串的当前长度
// 预处理:在原始字符串的每两个字符之间以及首尾插入分隔符'#'
// 这样可以统一处理奇数长度和偶数长度的回文串
s[0] = '$'; // s[0]放哨兵字符$,防止越界
s[++k] = '#'; // 第一个字符为#
for (int i = 1; i <= n; i++) // 遍历原始字符串的每个字符
{
s[++k] = a[i]; // 放入原始字符
s[++k] = '#'; // 在每个字符后插入#
}
n = k; // 更新n为处理后字符串的长度
get_d(s, n); // 执行Manacher算法计算回文半径
// 统计最长回文串长度:d[i]-1即为以i为中心的最长回文串的原始长度
// 因为d[i]包含了分隔符#,实际原始字符个数为d[i]-1
for (int i = 1; i <= n; i++)
ans = max(ans, d[i]);
cout << ans - 1 << endl; // 输出最长回文串的原始长度
return 0;
}
【运行结果】
aaa
3
浙公网安备 33010602011771号