字符串专题-学习笔记: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 f_i fi 表示以 i i i 为回文中心的字符串的最长半径(不是长度)。

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

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

那么假设我们已经处理完了 [ 1 , i − 1 ] [1,i-1] [1,i1] 内的所有 f i f_i fi,如何处理 f i f_i fi 呢?

看图。

  1. 如果 i < M a x n i<Maxn i<Maxn

在这里插入图片描述

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

此时考虑两种情况:

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

在这里插入图片描述

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

显然,上图的两个紫色部分代表的字符串相同,而且互相全等,此时的回文半径为 M a x n − i Maxn-i Maxni

那么因此有 f i = M a x n − i f_i=Maxn-i fi=Maxni

两者取 min ⁡ \min min 即可得到 f i f_i fi 的初值。

  1. i ≥ M a x n i \geq Maxn iMaxn

这个时候我们什么也不知道,只能令 f i = 1 f_i=1 fi=1

综上,当 i < M a x n i <Maxn i<Maxn f i = min ⁡ ( f j , M a x n − i ) f_i=\min(f_j,Maxn-i) fi=min(fj,Maxni)

i ≥ M a x n i \geq Maxn iMaxn f i = 1 f_i=1 fi=1

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

但显然这个 f i f_i fi 肯定是有问题的,因为当 f i = 1 f_i=1 fi=1 f i = M a x n − i f_i=Maxn-i fi=Maxni 时可能会有更长的回文半径。

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

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

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

最后不要忘记更新 i d , M a x n id,Maxn id,Maxn

上述做法我将其称之为『推移』操作,因为这个操作将 M a x n Maxn Maxn 往右边推移了。

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

2.3 复杂度证明

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

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

考虑 f i f_i fi 初值的 3 种情况:

  1. f i = f j f_i=f_j fi=fj

此时因为 f j f_j fj 已经达到最大,那么 f i f_i fi 也达到最大,因此无法往外推移,不计复杂度。

  1. f i = M a x n − i / f i = 1 f_i=Maxn-i/f_i=1 fi=Maxni/fi=1

这个时候就需要推移了。

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

因此在这两种情况下, M a x n Maxn Maxn 一定会变大,而且也只能变大。

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

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

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

2.4 代码

请注意代码里面的枚举细节, i i 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 fi 初值,暴力推移得到正确的 f i f_i fi
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值