题解:洛谷 P3805 Manacher

【题目来源】

洛谷: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

【核心思想】

  1. 问题分析:给定长度为 \(n\) 的字符串 \(S\)(仅含小写字母),求 \(S\) 中最长回文子串的长度。这是一个Manacher 算法模板题,关键在于利用回文串的对称性,将暴力中心扩展的 \(O(n^2)\) 优化到线性 \(O(n)\)

  2. 算法选择

    • 字符间隔插入:在相邻字符间及首尾插入分隔符 #,将奇偶长度回文统一为奇数长度回文处理
    • Manacher 算法:维护当前最右回文串的边界 \([l, r]\),对每个中心位置 \(i\) 利用对称位置 \(d[r-i+l]\) 初始化回文半径,减少重复扩展
  3. 关键步骤

    • 预处理:原字符串 \(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\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个位置最多被扩展一次,均摊线性
    • 空间复杂度:\(O(n)\),存储处理后的字符串和回文半径数组
  5. 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
posted @ 2026-08-07 11:44  团爸讲算法  阅读(7)  评论(0)    收藏  举报