manacher算法
原题链
算法原理
一
首先规定:
1.maxx为已经找过的符合回文串条件的最右边的坐标的右边一位 -> 举个例子:ababc 当查找到子串aba时对称轴为b所在的坐标, 此时maxx为原串中第二个b的位置,即为上文所说符合回文串条件的最右边的坐标的右边一位;
2.i为以i为对称轴来向两边寻找回文串;
3.p[i]为以i为对称轴的最大回文半径,即p[i]为以i为对称轴可以找到的最长回文(包括填充的额外字符)的长度的一半+1
举个例子:$$a(a)b(**b)a(a)~ 坐标从0~14,p[7]为以加粗的(**为对称轴的最长回文半径(包括这个加粗的补充字符为:)b(a)a$,因此p[7] = 7,那么这个真实的不包括补充字符的回文串长度为p[7] - 1 = 7 - 1 = 6);
3.mid为上一次更新maxx值时i的值,也就是当前拓展到最长回文串的对称轴;
4.ans存储答案即最长回文串的长度(不包括补充的字符);
二
算法过程
以abba为例,这个字符串存储在ss中,预处理后的字符串存在s中,其余变量意义均与上文所说相同.
首先为了避免对于奇数个和偶数个字符的字符串的讨论,我们进行一下预处理:即在开头加入一个字符,再在原字符串的各个字符之间加入额外的分隔符,(注意:额外插入的字符不能是字符串中可能出现的字符,最后一个插入字符也不能与之前的相同,否则计算出来的回文串长度会变大)因此abba操作之后就会变为 $$a(b)b(a)~;
然后就可以进行马拉车的主体部分.首先i = 1,因为i = 0是左边不存在字符,故不将i = 0作为对称轴,所以程序从i = 1开始.
i = 1 : maxx = -1,所以不存在已知位于i右侧的已匹配好的回文串,所以给p[i]赋初值为1,即自身;再通过while循环从i向两侧枚举回文串(因为回文串有对称性啊).接着判断是否枚举到的新的回文串端点已经超过了之前算出的maxx,如果超过则更新maxx,使它为已匹配的回文串的右端点的下一位,并将mid赋值为当前枚举到的对称轴i.最后更新ans = max(ans,p[i] - 1);至于为什么要p[i] - 1可以参考上文;这样第一次就做完了,此时ans = 1,p[1] = 1,maxx = 2,mid = 1;
i = 2 : 此时s[i] = a , s[i - 1] = $ , s[i + 1] = $,而i仍没有处于已匹配的回文串中,所以p[2]仍赋初值为1,但此时s[i]两端的字符相等,所以在while循环中p[2]++变成2;此时判断已匹配的回文串的右端是否超出了maxx,至于为什么p[i] + i可以表示已匹配的回文串的右端点的下一位呢?这里稍作解释:p[i]为以i为对称轴的最长回文串半径(包括s[i]本身),因此将最长半径+当前坐标i即为最优端点的下一位,但为什么是下一位呢?我们注意到最长半径其实是包括了i这一位的,但在加上坐标i时又重复算了一次,因此变成了当前已匹配的回文串的右端点的下一位(太绕口辣),因此这个意义刚好符合maxx的意义,所以直接复制给maxx进行更新,mid也变为i,ans同样更新.此时maxx = 4,p[2] =2,mid = 2,ans = 1 ;
i = 3 : maxx = 4,所以i包含在已经匹配过的回文串中间,因此p[i]直接更新,而不是赋值为1,更新代码为 -> p[i] = max(maxx - i,p[mid * 2 - i]);
maxx是之前已匹配的回文串的最右端的下一位,而i为当前枚举的对称轴,两者相减即为以i为对称轴,可取的,已知匹配完的最长的回文串半径,此处maxx - i = 1;而p[mid * 2 - i]则表示如图.根据中点坐标公式,mid = (i + i') / 2,移项变形后得到 2 * mid = i + i' --> i' = 2 * mid - i ;所以p[2 * mid - i]表示p[i'],又因为maxx大于i,因此i与i'均包含于以mid为对称轴的回文串中,因此p[i']一定等于p[i],所以可以用i'的值更新i;此时mid = 2表示maxx是以mid为对称轴的;i' = 1,p[i'] = 1,说明p[i]已知的最大回文半径是1,再向两边拓展发现没有相同的字符,此时符合回文条件的子串的最右端点已经变成i = 3,这个字串就是s[i],i=3这一个字符,maxx表示的是最右端点的更右一位的坐标,所以被更新为4,mid更新为i,即mid = 3;ans不变,因为新找的子串长度为1,没有比ans大.
至此,manacher算法的大致过程就基本讲述完毕,几个关键的变量和语句也已经讲完辣,实在还没懂的可以康康代码
代码
#include <bits/stdc++.h>
using namespace std;
char ss[71000010],s[71000010];
int p[71000000];
int len,ans = -1;
void init(){//预处理,将每个字符之间插入一个分隔字符,解决奇回文和偶回文的问题
len = 1;
s[0] = '$';s[1] = '$';
int tmp = strlen(ss);
for(int i = 0;i < tmp;i++){
s[++len] = ss[i];
s[++len] = '$';
}
s[++len] = '~';//最后一个额外字符与其它的不一样,防止影响答案,不然额外字符也会被匹配成回文并计算长度
}
void horse(){
int maxx = 1,mid = 1;
for(int i = 1;i < len;i++){
if(i < maxx){
p[i] = min(maxx - i,p[mid * 2 - i]);
}
else p[i] = 1;
while(s[i - p[i]] == s[i + p[i]])//向两边拓展回文
p[i]++;
if(maxx < i + p[i]){//更新maxx和mid
mid = i;
maxx = i + p[i];
}
ans = max(ans,p[i] - 1);//更新答案
}
}
int main(){
ios::sync_with_stdio(false);
cin>>ss;
init();
horse();
cout<<ans<<endl;
return 0;
}
Ps:这个博客写了3周,中间隔了一周半没动,搞不赢啊,逻辑可能有些断续,请见谅QWQ
另外,这个洛谷上的题不能用(string)写,因为它是开双倍空间的,会(RE)