面试加分项!Manacher:O(n)求最长回文子串的对称之美

先把定位说清楚,免得你白紧张:Manacher是面试加分项,不是常规要求 。

绝大多数面试里,答出"中心扩散O(n²)"就已经过关了;但如果你能在白板上把Manacher讲明白,那是实打实的亮点。

更值得你带走的是Manacher背后的思想------它和 KMP是同一条脉络:

已经算出来的信息,绝不再算第二遍。

KMP靠"最长相等前后缀"让主串指针不回退;Manacher靠"回文的对称性"让扩散半径不从头开始。

两者都是用一份预处理信息,把重复劳动一次性省掉。


📦 题目速览 LeetCode 5(30秒读懂)

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

示例: "babad" → "bab"("aba"也合法)

示例: "cbbd" → "bb"

示例: "a" → "a"

约束: n ≤ 1000,仅数字和英文字母。

"回文"就是正读反读都一样:"aba"是,"abba"也是。注意前者长度是奇数(中心是单个字符b),后者是偶数(中心是bb之间的空隙)------奇偶中心不同,是所有回文解法的第一个绊脚石。


🧠 核心思路:从暴力到Manacher的进化链

① 暴力枚举 :枚举所有子串O(n²)个,每个判回文O(n) → O(n³)。

② 动态规划 :dp[i][j]表示s[i..j]是否回文,转移dp[i][j] = dp[i+1][j-1] && s[i]==s[j] → O(n²) 时间 + O(n²)空间。

③ 中心扩散 :枚举每个可能的中心(n个奇数中心 + n-1个偶数中心),各向两边扩散 → O(n²) 时间、O(1) 空间。这是"够用解"。

④ Manacher :中心扩散的瓶颈在于------每个中心的半径都从0开始重新数 。

回文有对称性:如果我已知一个以C为中心、右边界到R的大回文,那么对于C右侧的某个位置i,它关于C的镜像点mirror = 2C - i的半径已经被算过了 ,可以直接拿来当i的初始半径。

Manacher的三个组件

  1. 预处理 :在字符间插入分隔符(如#),把奇偶长度统一成奇数------"aba"变#a#b#a#,"abba"变#a#b#b#a#
  2. 维护最右边界 :记录当前已探明的、右边界最靠右的那个回文的中心center与右边界right
  3. 镜像复用 :若i < right,则p[i]的初始值可以取min(right - i, p[mirror]),不必从1开始扩散

那个min里的两个含义要分清 :p[mirror]是镜像点的半径(能复用多少),right - i是"别超出已知大回文的边界"(超出部分未经验证,不能用)。

一句话核心:用分隔符统一奇偶 → 用最右边界记住"探明区域" → 用镜像把新位置的半径初始化,从而让扩散总量变成线性。


🖼️ 图解算法(手把手走一遍)

以s = "abba"为例。预处理后t = "#a#b#b#a#",长度n = 9:

下标 0 1 2 3 4 5 6 7 8
字符 # a # b # b # a #

逐位推进(p[i]为以i为中心的回文半径,含中心;right为最右已探明边界):

i ti 在边界内? 镜像点 pmirror 初始pi 扩散后pi 更新后 (center, right)
0 # 否 --- --- 1 1 (0, 1)
1 a 否 --- --- 1 2 (1, 3)
2 # 是(2<3) 0 1 min(1,1)=1 1 (1, 3)
3 b 否(3=3) --- --- 1 2 (3, 5)
4 # 是(4<5) 2 1 min(1,1)=1 5 (4, 9)
5 b 是 3 2 min(4,2)=2 2 (4, 9)
6 # 是 2 1 min(3,1)=1 1 (4, 9)
7 a 是 1 2 min(2,2)=2 2 (4, 9)
8 # 是 0 1 min(1,1)=1 1 (4, 9)

关键看i = 4 :它落在已知回文(center=3, right=5)内,镜像点mirror = 2×3-4 = 2,p[2] = 1,于是初始半径直接给到1。随后扩散一路匹配到p[4] = 5,覆盖了t[0..8],对应原串"abba"------中心回文被一次性发现。

还原答案 :p[i]是在t中的半径,转回原串有固定公式:

复制代码
起始下标start = (i - p[i] + 1) / 2
原串长度len   = p[i] - 1

i=4 时:start = (4-5+1)/2 = 0,len = 5-1 = 4 → s[0:4] = "abba" ✓


💻 代码实现(Python + Java)

Python版

python 复制代码
class Solution:
    # ---------- 够用解:中心扩散O(n²) / O(1) ----------
    def longestPalindrome_expand(self, s: str) -> str:
        n = len(s)
        if n < 2:
            return s
        start, max_len = 0, 1
        for i in range(n):
            # 奇数长度回文:中心是单个字符
            l, r = i, i
            while l >= 0 and r < n and s[l] == s[r]:
                l -= 1
                r += 1
            if r - l - 1 > max_len:          # 越界后r-l-1才是真实长度
                max_len, start = r - l - 1, l + 1
            # 偶数长度回文:中心是两个字符之间
            l, r = i, i + 1
            while l >= 0 and r < n and s[l] == s[r]:
                l -= 1
                r += 1
            if r - l - 1 > max_len:
                max_len, start = r - l - 1, l + 1
        return s[start:start + max_len]

    # ---------- 最优解:ManacherO(n) ----------
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ""
        t = '#' + '#'.join(s) + '#'          # 插入分隔符统一奇偶
        n = len(t)

        p = [1] * n                          # p[i]:半径(含中心)
        center = right = 0                   # 最右回文的中心与右边界
        best_i = 0

        for i in range(n):
            if i < right:                    # 在已知边界内 → 镜像复用
                mirror = 2 * center - i
                p[i] = min(right - i, p[mirror])
            while (i - p[i] >= 0 and i + p[i] < n
                   and t[i - p[i]] == t[i + p[i]]):
                p[i] += 1                    # 从已有半径继续扩散
            if i + p[i] > right:             # 刷新最右边界
                center, right = i, i + p[i]
            if p[i] > p[best_i]:
                best_i = i

        start = (best_i - p[best_i] + 1) // 2
        return s[start:start + p[best_i] - 1]

Java版

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) return "";

        StringBuilder sb = new StringBuilder("#");
        for (char c : s.toCharArray()) sb.append(c).append('#');
        String t = sb.toString();
        int n = t.length();

        int[] p = new int[n];
        for (int i = 0; i < n; i++) p[i] = 1;
        int center = 0, right = 0;
        int bestI = 0;

        for (int i = 0; i < n; i++) {
            if (i < right) {
                int mirror = 2 * center - i;
                p[i] = Math.min(right - i, p[mirror]);
            }
            while (i - p[i] >= 0 && i + p[i] < n
                   && t.charAt(i - p[i]) == t.charAt(i + p[i])) {
                p[i]++;
            }
            if (i + p[i] > right) {
                center = i;
                right = i + p[i];
            }
            if (p[i] > p[bestI]) bestI = i;
        }

        int start = (bestI - p[bestI] + 1) / 2;
        return s.substring(start, start + p[bestI] - 1);
    }
}

⚠️ 防坑提醒(必看):

  • p的初值必须是 1(中心自身就是一个长度为1的回文),不是0。
  • min(right - i, p[mirror])里的right - i不能省------它保证初始半径不越过"已探明区域"。
  • 转回原串的公式:start = (i - p[i] + 1) // 2,len = p[i] - 1。

⏱️ 复杂度分析(面试必问)

  • 时间O(n) :这是Manacher最反直觉的地方------明明有内层while循环,为什么是线性?
    因为每当扩散成功一次,right就至少右移一格,而right最多右移n 次;
    加上镜像复用时直接跳过扩散,总扩散次数被摊还成O(n)。
    这属于摊还分析的结论(和双栈队列的均摊O(1)是同一类思想)。
  • 空间O(n) :p数组 + 预处理串t。
解法 时间 空间
暴力枚举 O(n³) O(1)
区间DP O(n²) O(n²)
中心扩散 O(n²) O(1)
Manacher O(n) O(n)

实测:O(n)的保证值多少钱?

串长4000(CPython 3.13):

输入 最长回文长度 Manacher 中心扩散 结论
随机ab串 26 0.0027s 0.0018s 中心扩散快1.5×
a/b/aa混合 40 0.0036s 0.0031s 中心扩散略快
全a(回文极长) 4000 0.0023s 0.7293s Manacher快314倍

随机串上,Manacher反而慢 ------因为回文平均长度只有26,中心扩散接近O(n),而Manacher要先构造2n+1的分隔串、再走2n+1个中心,预处理常数白白付了出去。

但回文一旦变长,中心扩散立刻崩 :全a时它是实打实的O(n²)(每个中心都要扩2000次),耗时从0.0018s涨到 0.7293s,涨了400倍 ;而Manacher因为镜像复用,耗时反而略微下降。

这就是理论保证的价值:中心扩散的性能取决于数据,Manacher的性能不取决于数据。 生产环境的输入往往不是随机的------文本里可能出现"aaaa...aaa"、回文结构密集的HTML/XML片段、或用户恶意构造的退化输入。买Manacher,买的不是"平均更快",而是"任何输入都不会退化"。


🚀 隐藏价值:p数组还能一行统计回文子串数(LC.647)

p[i]不只是"半径",它直接等于以t[i]为中心、在原串里的回文子串个数:

python 复制代码
def countSubstrings(s: str) -> int:
    t = '#' + '#'.join(s) + '#'
    n = len(t)
    p = [1] * n
    center = right = 0
    for i in range(n):
        if i < right:
            mirror = 2 * center - i
            p[i] = min(right - i, p[mirror])
        while i - p[i] >= 0 and i + p[i] < n and t[i - p[i]] == t[i + p[i]]:
            p[i] += 1
        if i + p[i] > right:
            center, right = i, i + p[i]
    return sum(x // 2 for x in p)      # 每个中心贡献 p[i]//2 个回文

公式是sum(p[i] // 2):逐个整除再求和 ,不能写成sum(p) // 2(先求和再除会多算,因为每个中心的取整损失被累积放大了)。

用s = "aaa"核一遍:t = "#a#a#a#",跑出p = [1, 2, 3, 4, 3, 2, 1],逐个整除求和0+1+1+2+1+1+0 = 6;而暴力枚举"aaa"的回文子串是"a"×3 + "aa"×2 + "aaa"×1 = 6 ✓(若误写成 sum(p) // 2会得到8,错在两个半截回文被凑成了一整个)。

这是Manacher的隐藏价值 :跑完一遍p数组,整串的回文结构信息就全在里面了。


🚀 举一反三:5道高频变体题

题号 题目 与本题的关系
LC.647 回文子串(计数) p数组直接求和,见上
LC.516 最长回文子序列 注意是子序列,用区间DP,Manacher不适用
LC.214 最短回文串 KMP的应用,与 Manacher不同路
LC.131 分割回文串 回文判定 + 回溯
LC.5 最长回文子串 本题

💬 面试追问模拟(提前准备,惊艳全场)

Q1:为什么p[i]要取min(right - i, p[mirror])?

两个上界含义不同:p[mirror]是"镜像点能复用多少半径",right - i是"不能超过已探明区域的右边界"。只有p[mirror]完全落在边界内时才完全复用;一旦超出,超出的部分没有对称性保证,必须老实扩散。

Q2:预处理插入#的作用是啥?

统一奇偶。奇回文(如aba)中心是字符,偶回文(如abba)中心是空隙;

插入分隔符后,两种回文在t中都变成"以某个字符为中心的奇回文",代码只需处理一种情况。

Q3:Manacher和KMP有什么共通之处?

都是"已算过的信息不再重算 ":KMP用next数组记住最长前后缀,失配时模式串滑到该位置;

Manacher用p数组记住半径,新位置从镜像半径起步。两者都把O(n²)或更差的重复劳动压到O(n)。

Q4:工程里用得多吗?

不算高频。但它出现在一些特定领域:生物信息学的回文序列检测、文本处理的对称结构分析。面试中的价值主要是展示你对摊还分析和对称性利用的理解深度。


🧩 实战小技巧(刷题党必备)

  • 口诀:插分隔符统奇偶,最右边界记探明,镜像复用省扩散。
  • 模板:Manacher = 预处理 + 最右边界 + 镜像复用 + 扩散。
  • 防坑 :p初值必须为1;right - i不能省;还原公式别记错。

📈 实际应用场景(不止是刷题)

  • 生物信息学:DNA回文序列识别(限制性内切酶切点)
  • 文本处理:对称结构分析
  • 数据压缩:对称模式检测

🎁 今日思考题

Manacher的p数组其实还能直接算出"字符串里一共有多少个回文子串"(LC.647)。

提示 :p[i]是半径,半径内的每一层都是一个回文。

你能想到怎么从p[i]推出以i为中心的回文子串个数吗?

相关推荐
知守观1 小时前
Maven SNAPSHOT 更新排查:-U 无效、clean 无用,问题在 _remote.repositories(附完整链路)
java·后端·maven
牛战士coding1 小时前
从一句\“取消订单\“到退款通知:AI 写操作完整业务闭环
java·人工智能
啵啵啵鱼1 小时前
第三章 栈、队列和数组相关结构
java·数据结构·栈·队列
树獭哥1 小时前
Deep Agents 学习笔记(一):从 Agent Harness 到第一个 Deep Agent
人工智能·python
瑶山1 小时前
stable-diffusion.cpp 参数全集整理(sd-cli.exe / sd-server.exe)
java·数据库·stable diffusion·参数全集
CallFay云起未来1 小时前
2026年AI客服能不能减少人工回复?从知识检索到人机协同的技术实现
java·大数据·前端·人工智能·架构·文心一言
旖旎夜光2 小时前
【LangGraph实战】LangGraph 学习笔记(五):信息裁剪、人机交互与时间旅行
笔记·python·学习·ai·人机交互·langgraph
Helix2502 小时前
Python单元测试实战:pytest框架从入门到企业级测试体系搭建指南
python·单元测试·实战·mock·pytest·测试覆盖率·fixture
伯恩bourne2 小时前
对比 @Configuration(Full) vs @Component(Lite)
java·开发语言