Manacher 学习笔记
Manacher,又名马拉车算法,是一种能在 \(O(n)\) 的时间复杂度之内求出一个字符串的最长回文字串的巧妙算法,其与 exKMP 有一点相似之处
Part1:实现步骤
step1:改造字符串
首先,我们要对字符串进行改造
因为在原字符串中会有 奇回文串和偶回文串,要分两种情况,不好处理,所以我们要改造字符串,使其只有一种情况
那么我们怎么改造呢?
很简单:
只用在原串的开头结尾以及每个字符之间加上一个未在字符集中出现过的特殊字符即可
并且,我们规定改造出来的字符串下标从一开始,下标为 \(0\) 的地方放另外一个特殊字符来防止越界
改造示意如下:
改造好了之后我们会发现,所有回文串都是奇回文串了,有一个回文中心。
step2:回文半径
以 \(i\) 为中心的回文串的长度的一半,叫做回文半径(注意,这里的长度的一半要向上取整)
举个栗子
其中的 \(d[]\) 数组就是指的以 \(i\) 为中心的回文半径
step3:加速!
假设我们现在已经找到了一个在当前最靠右的回文串 \(T\)
就是图中的绿色部分,然后我们已经推完了其回文中心 \(6\) 以前的 回文半径,接下来我们要继续向后推点 \(7\) ,那该怎么办?
我们完全可以利用上回文的性质:
既然点 \(7\) 处在这个回文串中,那么它必然与关于回文中心对称的 \(5\) 相等.那么我们就可以利用上 \(5\) 的信息——— \(7\) 的左右两个也必然相等
这就是第一种情况:当 \(i\) 处在 \(T\) 之中时,便可利用上与之关于回文中心对称的点 \(i'\) 的信息。
当然,这种情况也分两种小情况:
- 当 \(i\) 加上 \(i'\) 的回文半径之后仍然在 \(T\) 之中,\(d[i]=d[i']\)
- 当 \(i\) 加上 \(i'\) 的回文半径之后不在 \(T\) 之中 那么在 先将 \(d[i]\) 赋值为 在 \(T\) 之内的长度,在 \(T\) 之外的部分直接 暴力 去求
在上面的那幅图中的 \(d[9]\) 又是为什么只有 \(1\) 呢?
因为 \(9\) 在字符串边缘啊,向右只有这么多字符了
那自然,还有 \(i\) 不处在 \(T\) 之中的情况,这种直接暴力解决
在处理完上面的情况之后,我们还要更新一下 \(T\)
如果 \(i+d[i]-1>r_t\) ,那么最靠右的回文串就是 以 \(i\) 为回文中心的回文串
最后得出结果:原串的最长回文串=新串的最大回文半径-1
Part3:时间复杂度分析
很显然的,\(i\) 从左到右扫一遍 \(O(n)\)
然后暴力的部分最多扫 \(n\) 个,因为 每次暴力都会将 最靠右的回文串向右挪,所以最多 \(O(n)\)
总复杂度 \(O(n)\)
结束!
上代码!
#include<bits/stdc++.h>
using namespace std;
string chuan;
string new_;
int d[23451400];
int main()
{
ios::sync_with_stdio(0);
cin>>chuan;
new_.push_back('%');//插入开头特殊字符
for(int ww=1;ww<=chuan.size();ww++)//改造回文串
{
new_.push_back('#');
new_.push_back(chuan[ww-1]);
}
new_.push_back('#');
//get_d;
d[1]=1;//d[1]=1
int max_=1;//最大的d值
for(int i=2,l=0,r=1;i<new_.size();i++)//l和r就是当前最靠右的回文串的左右端点
{
if(i<=r)//如果当前的 i 在这个回文串中
{
d[i]=min(d[r-i+l],r-i+1);//直接传递没有问题的部分
}
while(new_[i-d[i]]==new_[i+d[i]])//剩下的部分暴力枚举
{
d[i]++;
}
if(i+d[i]-1>r)//更新回文串位置
{
l=i-d[i]+1;
r=i+d[i]-1;
}
max_=max(max_,d[i]);
}
cout<<max_-1;
return 0;
}