字符串专题-学习笔记:Manacher 算法

1. 前言

Manacher 算法,俗称“马拉车”算法,是一种字符串算法,该算法可以在线性时间内求出一个串中最长回文串的长度,以及以每一个点为回文中心的奇长度回文串的长度。

实际上偶长度回文串的长度也能够求,后面会讲。

前置知识:无。

2. 详解

例题:P3805 【模板】manacher算法

2.1 奇偶化归

上面这个词是我自己编的,百度搜不到qwq

首先看这两个回文串:ABABABAABAABA

前面的回文串长度为 7,是个奇数,这样的回文串称之为奇长度回文串,简称奇回文串。

后面的回文串长度为 6,是个偶数,这样的回文串称之为偶长度回文串,简称偶回文串。

可以发现,奇回文串的回文中心是最中间的字符,偶回文串的回文中心是中间两个字符的中间(也就是 AA 中间的空隙)。

因为奇回文串与偶回文串回文中心性质不相同(一个字符,一个空隙),因此我们需要对字符串做一点手脚:在每两个字符中间以及左右两端插入一个未曾出现过的字符。

比如说还是上面两个字符串 ABABABAABAABA,我们按照上述描述插入 # 这个字符,于是这两个字符串就变成了 #A#B#A#B#A#B#A##A#B#A#A#B#A#

此时你会发现这两个字符串统一变成了奇回文串。

因此 Manacher 算法的第一步就是插入字符,使所有可能回文串变成奇回文串。

我将其称之为『奇偶化归』,因为这个方法统一了奇回文串和偶回文串。

下面若无特殊说明,默认字符串为奇偶化归之后的字符串。

2.2 翻转推移

还是我自己起的qwq

实际上翻转推移分为 2 块:翻转、推移。

接下来是 Manacher 算法的核心步骤。

\(f_i\) 表示以 \(i\) 为回文中心的字符串的最长半径(不是长度)。

又设两个值 \(id,Maxn\),其中 \(Maxn\) 是所有回文中心在 \([1,i-1]\) 中的回文串所能到达的最右端距离的最大值,而 \(id\) 是这个回文中心。我称这个字符串为最右字符串。

显然,以 \(i\) 为回文中心,这个回文串能够到达的最右端距离是 \(f_i+i\)

那么假设我们已经处理完了 \([1,i-1]\) 内的所有 \(f_i\),如何处理 \(f_i\) 呢?

看图。

  1. 如果 \(i<Maxn\)

在这里插入图片描述

设在以 \(id\) 为中心的回文串中 \(i\) 的对称位置为 \(j\),那么由中点公式有 \(j=2 \times id-i\)

此时考虑两种情况:

  1. 若以 \(j\) 为回文中心的回文串在最右字符串中间,显然有 \(f_i=f_j\)
  2. 若不是,继续看下图:

在这里插入图片描述

上图的红色部分代表以 \(j\) 为中心的回文串。

显然,上图的两个紫色部分代表的字符串相同,而且互相全等,此时的回文半径为 \(Maxn-i\)

那么因此有 \(f_i=Maxn-i\)

两者取 \(\min\) 即可得到 \(f_i\) 的初值。

  1. \(i \geq Maxn\)

这个时候我们什么也不知道,只能令 \(f_i=1\)

综上,当 \(i <Maxn\)\(f_i=\min(f_j,Maxn-i)\)

\(i \geq Maxn\)\(f_i=1\)

上述操作就是『翻转』操作,因为其充分利用了回文串的性质,对称翻转得到了尽可能大而又准确的 \(f_i\)

但显然这个 \(f_i\) 肯定是有问题的,因为当 \(f_i=1\)\(f_i=Maxn-i\) 时可能会有更长的回文半径。

此时我们就需要推移得到真正的 \(f_i\),这一块 暴力 做即可。

我没骗你,暴力,而且复杂度是正确的,就是 \(O(n)\) 做法。

暴力做法就是直接暴力匹配 \(i+f_i\)\(i-f_i\),看看能不能继续向外扩展回文串即可。

最后不要忘记更新 \(id,Maxn\)

上述做法我将其称之为『推移』操作,因为这个操作将 \(Maxn\) 往右边推移了。

那么最长回文串长度就是 \(\max\{f_i\}-1\),即最大半径 -1,减一是因为一个要去除我们奇偶化归时加入的字符 #,另一个没有除以 2 是因为本身我们的字符串长度就是翻倍过的。

2.3 复杂度证明

上面提到了,有一个暴力推移操作,这个操作后面的部分很暴力,还能做到开头提出的 \(O(n)\) 算法吗?

复杂度仍然是 \(O(n)\) 的,证明如下:

考虑 \(f_i\) 初值的 3 种情况:

  1. \(f_i=f_j\)

此时因为 \(f_j\) 已经达到最大,那么 \(f_i\) 也达到最大,因此无法往外推移,不计复杂度。

  1. \(f_i=Maxn-i/f_i=1\)

这个时候就需要推移了。

但是需要注意的是,对于前一种情况,以 \(i\) 为回文中心的回文串最右端就是 \(Maxn\),而对于后一种情况,\(i\) 本身大于 \(Maxn\)

因此在这两种情况下,\(Maxn\) 一定会变大,而且也只能变大。

因为 \(Maxn\) 最大值为 \(n\)(整个串就是回文串),而 \(Maxn\) 只能往右边推移,因此更新 \(Maxn\) 也就是暴力的复杂度至多为 \(O(n)\)

综上,暴力总复杂度为 \(O(n)\),而且推移部分不会与暴力部分干扰。

这就说明了其实推移部分的 \(O(n)\) 与暴力部分的 \(O(n)\) 复杂度是独立的,因此总复杂度是相加不是相乘,为 \(O(n)\)

2.4 代码

请注意代码里面的枚举细节,\(i\) 是从字符串的第二个字符开始枚举的,如果从第一个字符开始枚举,会导致 i - f[i] 越界,造成代码错误。

代码:

/*
========= Plozia =========
    Author:Plozia
    Problem:P3805 【模板】manacher算法
    Date:2021/5/12
========= Plozia =========
*/

#include <bits/stdc++.h>
#define Max(a, b) (((a) > (b)) ? (a) : (b))

typedef long long LL;
const int MAXN = 2.3e7 + 10;
int len1, len2, f[MAXN];
char str1[MAXN], str2[MAXN];

int read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
// int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

void init()
{
    len1 = strlen(str1);
    str2[0] = '^'; str2[1] = '$';
    for (int i = 0; i < len1; ++i)
    {
        str2[(i << 1) + 2] = str1[i];
        str2[(i << 1) + 3] = '$';
    }
    str2[(len1 << 1) + 2] = '@';
    len2 = (len1 << 1) + 2;
}

void Manacher()
{
    int id = 0, Maxn = 0;
    for (int i = 1; i < len2; ++i)
    {
        if (Maxn > i) f[i] = Min(f[(id << 1) - i], Maxn - i);
        else f[i] = 1;
        for (; str2[i + f[i]] == str2[i - f[i]]; ++f[i]) ;
        if (f[i] + i > Maxn) { Maxn = f[i] + i; id = i; }
    }
}

int main()
{
    scanf("%s", str1);
    init(); Manacher(); int ans = 0;
    for (int i = 0; i <= len2; ++i) ans = Max(ans, f[i]);
    printf("%d\n", ans - 1);
}

3. 总结

Manacher 算法分为 2 步:奇偶化归,翻转推移。

  • 奇偶化归:将奇回文串与偶回文串化归为奇回文串。
  • 翻转推移:利用回文串性质翻转得到 \(f_i\) 初值,暴力推移得到正确的 \(f_i\)
posted @ 2022-04-17 15:33  Plozia  阅读(35)  评论(0编辑  收藏  举报