manacher算法详解+模板 P3805
记住manacher是一个很简单的算法。首先我们来了解一下回文字串的定义:若一个字符串中的某一子串满足回文的性质,则称其是回文子串。(注意子串必须是连续的,而子序列是可以不连续的)那么若给定一长度为n的字符串,要求出最长回文子串的长度,怎么做呢?首先想到的是暴力搜索,我就不赘述思路了。那如果n特别大呢?10的7次方怎么做?于是,我们需要了解一个贼有意思的鸡肋算法manacher,俗称“马拉车”,为什么说是贼有意思呢?因为它的思路实在是巧妙,又为什么说是鸡肋呢?因为它貌似只适用于求解最大回文子串的问题。
首先我们知道回文子串的判定和长度的奇偶性是有关系的,由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,在字符间插入一个字符(前提这个字符未出现在串里),常用的是"$""#"。举个例子:s="abbahopxpo"
,转换为newS="$#a#b#b#a#h#o#p#x#p#o#\0"
(这里在串首、尾加的字符"$"和"\0";只是设置边界,为了防止越界,而且显然不影响回文子串,下面会有说明),如此,s 里起初有一个偶回文abba
和一个奇回文opxpo
,被转换为"#a#b#b#a#"
和"#o#p#x#p#o#"
,长度都转换成了奇数。
证明经过上述操作回文串的长度必为奇数:
若原回文串的长度为奇数n(原串 aaa),首尾间共有偶数n-1个空位被加上"#",加上首前和尾后各1个"#"(新串 #a#a#a#),可见新的长度为2n+1,显然是奇数;
若原回文串的长度为偶数n(原串 aa),首尾间共有奇数n-1个空位被加上"#",加上首前和尾后各1个"#"(新串 #a#a#),可见新的长度也为2n+1,显然是奇数。得证。
我们定义一个辅助数组int p[]
,p[i]
表示以news[i]
为中心的最长回文的半径,例如
易得P[i]-1即以i为中心的在原串中的回文子串的长度:例如P[5]=5,则4就是以5这个位置为中心在原串中的最长回文子串的长度(abba),为什么这是对的呢?因为我们知道P[i]*2-1为新串中以i为中心的最长回文子串的长度,设该回文子串原长为n,则由上面的证明可知新串的长度=2n+1=P[i]*2-1,移项化简得n=P[i]-1。
于是重点来了:我们如何快速的求出P[]数组。这时就要用到DP的思想了,我们一般会想到这样求解p[i]
,先初始化p[i]=1
,再以news[i]
为中心判断两边是否相等,相等就p[i]++
。这就是普通的思维,但是我们想想,能否避免重复操作让p[i]
的初始化不是 1,让它更大点,看下图:
设置两个变量,mx 和 id 。
mx 代表以news[id]
为中心的最长回文最右边界,也就是mx=id+p[id]。
假设我们现在求p[i]
,也就是以news[i]
为中心的最长回文半径,如果i<mx
,如上图,那么
if(i<mx) p[i] = min(p[id*2-i] , mx-i);
else p[i] = 1;
怎么理解呢?我们看图,因为mx是以id为中心的最长回文半径,若当前的i比mx要小,说明以i为中心的最长回文子串的一部分已经出现在以id为中心的回文子串中了,注意图中下面标注的两条短黑线,因为我们是线性dp,j点一定被访问过且P[j]被处理过,而j与i关于id对称(i+j=2*id),所以j=id*2-i,由于回文串的对称性,以i为中心的最长回文子串中的半径最小值一定是p[j]和mx-i中的最小值; 而若i>mx,就只能将p[i]赋为1来更新了。(感觉解释了和没解释一样啊,由于博主表述能力较差,我们不如举例)
就比如一个回文子串:"#a#a#a#a#a#",我们以中间的a位置为id,所以id=6,mx=12。假设目前访问到了i=8的位置,则与其对称的j=2*6-8=4,而p[4]在访问8之前已经处理,p[4]=4,mx-i=4,所以取最小值4,将p[8]初始值赋为4。因为很容易看出在当前已经可以确定的是以8为中心的最长回文串的半径至少为4(#a#a#a#),当然有可能更大,我们之后判断news[i+P[i]]==new
s[i-P[i]]是否成立,若成立就p[i]++。
讲的好心累啊,我学manacher时完全自学,也没有什么解释,完全是靠自己看懂的,还是得自己结合图和代码理解啊,先发一波核心代码。
int manacher() { int len=init(); int ans=-N,id,mx=0; for(int i=1;i<len;i++) { if(i<mx)p[i]=min(p[id*2-i],mx-i); else p[i]=1; while(news[i-p[i]]==news[i+p[i]])p[i]++; if(mx<i+p[i])id=i,mx=i+p[i]; ans=max(ans,p[i]-1); } return ans; }
再发两种情况的图片自行理解一番:
这是初值p[i]=p[id*2-i]的图:
再来看这张图,我们发现,如果mx不更新,就不会出现本质不同的回文子串,因为前面已经出现过了;而每扩展一次mx,最多新出现一个本质不同的回文子串。
于是得到性质:一个字符串最多只有n个本质不同的回文子串。这个性质很重要,有些题会用到,需要这个性质去分析。
(虽然我也不知道有啥用……)
算法复杂度分析:知乎
我自己简略地讲一下,因为i与mx只有两种情况,而每次检索已经保证了单调递增,可以知道每个点最多被访问两次,while()循环本身的时间复杂度在没有前提条件的情况下确实是但是这里的
(也就是上面答案中的
),是不断往后走而不可能往前退的,它自身的值的变化是递增的。那么你可以明白,要进入while循环,
的值必然是比
大的,也就是说整个程序结束为止,while循环执行的操作数为
次(线性次),而字符串中的每个字符,最多能被访问到2次。时间复杂度必然为
终于讲完了,上模板题洛谷P3805
题目描述
给出一个只由小写英文字符a,b,c...y,z组成的字符串S,求S中最长回文串的长度.
字符串长度为n
输入输出格式
输入格式:一行小写英文字符a,b,c...y,z组成的字符串S
输出格式:一个整数表示答案
输入输出样例
aaa
3
说明
字符串长度len <= 11000000
代码:
#include<bits/stdc++.h> #define il inline #define ll long long #define debug printf("%d %s\n",__LINE__,__FUNCTION__) using namespace std; const int N=23000005; char s[N],news[N]; int p[N]; il int init() { int len=strlen(s); news[0]='$',news[1]='#'; int j=2; for(int i=0;i<len;i++)news[j++]=s[i],news[j++]='#'; news[j]='\0'; return j; } il int manacher() { int len=init(); int ans=-N,id,mx=0; for(int i=1;i<len;i++) { if(i<mx)p[i]=min(p[id*2-i],mx-i); else p[i]=1; while(news[i-p[i]]==news[i+p[i]])p[i]++; if(mx<i+p[i])id=i,mx=i+p[i]; ans=max(ans,p[i]-1); } return ans; } int main() { scanf("%s",s); printf("%d",manacher()); return 0; }