Manacher Algorithm

Manacher Algorithm

Gleen K. Manacher (1975), "A new linear-time 'on-line' algorithm for finding the smallest initial palindrome of a string"

一种关于一个字符串 s 的回文子串的算法, 可以线性求两个数组 f1if2i, 分别表示以第 i 位为中心的最长奇回文子串和最长偶回文子串的半径 (长度除以 2, 向上取整). 对于以 i 为中心的回文子串 [l,r], 规定 i=l+r2.

根据回文串的性质可以知道 f1if2i 也恰好是以 i 为中心的回文子串的个数. 这条性质非常重要.

朴素

在求字符 if1 时, 每次从 f1i=1 开始枚举, 只要 si+f1i=sif1i, 就让 f1i 增加 1.

对于 f2 同样如此, 只是边界换成 f2=0, 自增条件变成 si+f2i=sif2i1.

容易看出, 这样最坏的复杂度可达 O(n2), 在 s 形如 aaaa...aaaa 时可以出现.

优化

还是先考虑 f1 的求法.

由于朴素算法外层循环是从小到大按顺序的, 所以可以认为求 f1i 时, f1j (j[1,i)) 已经求出来了.

设当前 j+f1j1 最大的 (对应回文子串右边界最大) 的 jFrontier, 则对 i 有两种情况:

i>Frontier+f1Frontier1

这时的 i 不在任何已知的回文子串中, 则直接朴素求 f1i.

iFrontier+f1Frontier1

这时的 i 在以 Frontier 为中心的回文串内, 则必有 si=s2Frontieri. 内涵是因为 iFrontier 的右边, 又在以 Frontier 为中心的奇回文串内, 则在 Frontier 左边的对应位置也应该有一个同样的字符. 设这个字符位置为 Reverse=2Frotieri.

因为 ReverseFrontier 左边, 所以 f1Reverse 也一定求出来了, 所以 [Reversef1Reverse+1,Reverse+f1Reverse1] 是回文串, 再加上 [Frontierf1Frontier+1,Frontier+f1Frontier1] 也是回文串, 所以 [Reversef1Reverse+1,Reverse+f1Reverse1][Frontierf1Frontier+1,2ReverseFrontier+f1Frontier1] 皆为回文串. 二者的中心都是 Reverse, 设二者的交集为 [PalindromeL,PalindromeR], 则 [2FrontierPalindromeR,2FrontierPalindromeL] 也是回文串.

这时 [2FrontierPalindromeR,2FrontierPalindromeL] 的中心是 2FrontierPalindromeL+PalindromeR2=2FrontierReverse=i.

这时, 可以断定 f1iReversePalindromeL+1.

接下来, 考虑 f1i 是否能取更大值.

  • Reversef1Reverse+1<Frontierf1Frontier+1

    这时如果 f1i>ReversePalindromeL+1, 则 sFrontier+f1Frontier=s2iFrontierf1Frontier=s3Frontier2i+f1Frontier. 字符 3Frontier2i+f1Frontier 关于 Reverse 的对称字符就是 Frontierf1Frontier, 因为 Reversef1Reverse+1<Frontierf1Frontier+1, 所以字符 Frontierf1Frontier 也和前面提到的三个字符相同. 这样, sFrontierf1Frontier=sFrontier+f1Frontier. 但是 f1Frontier 则说明 sFrontierf1FrontiersFrontier+f1Frontier, 相矛盾, 所以 f1Frontier 不能更大.

  • Reversef1Reverse+1>Frontierf1Frontier+1

    根据已经求出的 fReverse 得, sReversef1ReversesReverse+f1Reverse. 又因为 Reversef1ReverseFrontierf1Frontier+1, 所以对称的 s2FrontierReverse+f1Reverses2FrontierReversef1Reverse, 所以这种情况 f1i 也不会更大.

  • Reversef1Reverse+1=Frontierf1Frontier+1

    这时比较特殊, 因为 sReversef1ReversesReverse+f1Reverse, sFrontierf1FrontiersFrontier+f1Frontier. 所以就算是 sFrontier+f1Frontier=s2iFrontierf1Frontier=sReverse+f1Reverse 也无所谓, 这不会和任何已经求出的 f1 值冲突. 所以在 f1i=ReversePalindromeL+1 的基础上继续朴素即可.

对于求 f2 的情况, 思路相同, 实现有细节上的差别, 不再赘述.

时间复杂度

f1 为例.

在大部分情况下, 每个 f1 的求出是 O(1) 的, 只要考虑朴素时的复杂度即可.

  • Frontier+f1Frontier1<i

    一定有 Frontier+f1Frontier1<i+f1i1, Frontier 被更新成 i, Frontier+f1Frontier 比原先多了至少 f1i, 即本次操作步数.

  • Frontier+f1Frontier1i, 但 Frontier+f1Frontier1=Reverse+f1Reverse1 时.

    朴素从原来的 f1i 开始, 这时 i+f1i1=Frontier+f1Frontier1, 接下来每执行一步, i+f1Frontier 都增加 1.

因为每次朴素执行 k 步后, Frontier+f1Frontier 都右移至少 k 位, 而 Frontier+f1Frontier 最大是 n, 所以朴素的总复杂度是 O(n).

综上, Manacher 可以在 O(n), 求出长度为 n 的字符串的所有回文子串.

模板

一个长度为 n 的字符串, 求最长回文子串长度. (n1.1107)

Manacher 求出 f1, f2, 第 i 个字符为中心的最长回文子串即为 max(2f1i1,2f2i), 求 f1, f2 时顺便统计即可.

代码, 缺省源省略

unsigned n, Frontier(0), Ans(0), Tmp(0), f1[11000005], f2[11000005];
char a[11000005];
int main() {
	fread(a+1,1,11000000,stdin);        // fread 优化 
  n = strlen(a + 1);                  // 字符串长度 
  a[0] = 'A';
  a[n + 1] = 'B';                     // 哨兵 
  for (register unsigned i(1); i <= n; ++i) {   // 先求 f1 
    if(i + 1 > Frontier + f1[Frontier]) {       // 朴素 
      while (!(a[i - f1[i]] ^ a[i + f1[i]])) {
        ++f1[i];
      }
      Frontier = i;                             // 更新 Frontier 
    }
    else {
      register unsigned Reverse((Frontier << 1) - i), A(Reverse - f1[Reverse]), B(Frontier - f1[Frontier]);
      f1[i] = Reverse - ((A < B) ? B : A);                      // 确定 f1[i] 下界 
      if (!(Reverse - f1[Reverse] ^ Frontier - f1[Frontier])) { // 特殊情况 
        while (!(a[i - f1[i]] ^ a[i + f1[i]])) {
          ++f1[i];
        }
        Frontier = i;                                           // 更新 Frontier 
      }
    }
    Ans = ((Ans < f1[i]) ? f1[i] : Ans);
  }
  Ans = (Ans << 1) - 1;                             // 根据 max(f1) 求长度 
  Frontier = 0;
  for (register unsigned i(1); i <= n; ++i) {
    if(i + 1 > Frontier + f2[Frontier]) {           // 朴素 
      while (!(a[i - f2[i] - 1] ^ a[i + f2[i]])) {
        ++f2[i];
      }
      Frontier = i;                                 // 更新 Frontier 
    }
    else {
      register unsigned Reverse ((Frontier << 1) - i - 1), A(Reverse - f2[Reverse]), B(Frontier - f2[Frontier]);
        f2[i] = Reverse - ((A < B) ? B : A);        // 确定 f2[i] 下界 
      if (A == B) { // 特殊情况, 朴素 
        while (a[i - f2[i] - 1] == a[i + f2[i]]) {
          ++f2[i];
        }
        Frontier = i;                               // 更新 Frontier 
      }
    }
    Tmp = ((Tmp < f2[i]) ? f2[i] : Tmp);
  }
  Tmp <<= 1;                                        // 根据 max(f2) 求长度 
  printf("%u\n", (Ans < Tmp) ? Tmp : Ans);          // 奇偶取其大 
  return Wild_Donkey;
}
posted @   Wild_Donkey  阅读(69)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示