前几天bestcode做到一道字符串的题目,需要O(n)的回文,正好看到网上的manacher算法,于是来学习一发
进入正题:
manacher算法
用法:一般用于求一个字符串的最大回文,操作过程中会记录以每个点为中心的回文半径,可用来进行其他操作
时间复杂度:O(n)
空间复杂度:记录字符串2*n,半径数组2*n
内容:
记p[i]为以i为中心的回文半径(aba中以b为中心的回文半径为2,那么如果是偶数个数的回文呢,比如abba?这里先讨论奇数,偶数后面再说,简单说就是用字符填充变成奇数)
因此以下的部分讨论的都是奇数个数的回文>>>>>>>>>>>>>>
那么无可避免先要for一遍,for(j=0;j<n;i++) 来求每个p[j],下面就是对p[j](令j=i+k)的求解
分析:假设已经求出了i+k前面的0~i+k-1的p,现在要求p[i+k],
那么首先根据前面的点求半径的时候有没有遍历到了i+k这个点可分为两大类:
一、 前面的点求半径的时候还没有遍历到了i+k这个点,即p[i]+i<i+k
(设i为前i+k个点中半径时遍历到最靠近字符串后面的点,不是回文最长!)
那么此时p[i+k]=1;
这时候说明此时的i+k还没找过半径,需要从头开始找while(s[i+k+p[i+k]]==s[i+k-p[i+k]]) p[i+k]++;
二、 前面的点求半径时已经遍历到了i+k这个点,即p[i]+i>=i+k
(设i为前i+k个点中半径时遍历到最靠近字符串后面的点,不是回文最长!)
这时候i+k前面有关于i对称的i-k,所以算i+k的回文半径时不需要从第一个开始找,那么是从p[i-k]个开始找吗?
答案是不一定的!
下面借用网上的图片来分析一下可能出现的两种情况:
<1>p[i]-k<p[i-k](此时p[i-k]>=p[i+k]),下面是结合图片的分析
首先解释一下图,图中的红线为i的回文半径,黑线为i求回文半径时遍历过的点,深蓝线是i-k的回文半径,那么此时i+k的回文半径一定是橙线,为什么呢?
首先橙线部分肯定是i+k的半径的一部分,由于对称性可以得到与左边i-k的橙线部分相同,
那么i+k的半径可能大于橙线部分吗?
假设i+k的回文半径大于橙线,如下图中橙线+紫线,那么由于对称性i-k也必定含有紫线部分,那么此时p[i]就不再是原来的p[i]了,而是如下图的黑线+紫线,这个与之前矛盾,所以不可能存在
即p[i+k]=p[i]-k;
<2>p[i]-k>=p[i-k](此时p[i-k]<=p[i+k]),下面是结合图片的分析
图中的红线为i的回文半径,黑线为i求回文半径时遍历过的点,深蓝线是i-k的回文半径,首先由于对称性i+k的回文半径一定包含蓝线部分,与此同时i+k的右边可能还有字符能组成更长的回文,最后形成图中的橙线部分,因此此时P[i+k]>=p[i-k],因此可以先赋值p[i+k]=p[i-k],然后while(s[i+k+p[i+k]]==s[i+k-p[i+k]])
p[i+k]++;继续往后面找。
于是由上可以总结出p[j]的求法:
void query_p(int n) //n为字符串的长度 { int id = 0; //id为上述i p[0] = 1; for(int j=1;j<n;j++) //j为上述i+k,所以k=j-id { if(p[id]+id<=j) p[j]=1; //第一种情况 else p[j]=min(p[2*id-j],p[id]-(j-id)); //第二种情况的两种小情况 while(s[j+p[j]]==s[j-p[j]]) p[j]++; //继续往后找 if(p[j]+j>p[id]+id) id=j; //更新最靠近字符串后面的点 } }
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<到这里为止都是把回文当做奇数个来算的,那么如果出现偶数个怎么办呢,具体做法是利用字符填充,字符可以用字符串中肯定不会出现的字符来填充,具体做法是:
假设字符串中只有字母,那么可以利用特殊符号填充,填充方法是在每个字符的前面插入一个一样的字符
比如填充字符时#时,原字符串是abba,填充后的字符变为#a#b#b#a#,然后就可以了,至于字母的原始回文半径就是现在的p[i]/2+1,填充字符不会影响结果。
不过这里需要注意的是:为了避免数组越界需要在填充后在字符再处理一下如上述*#a#b#b#a#‘\0‘,这样从第一个字母开始遍历的时候就不会出现问题了
这里贴一个填充后的求最长回文的代码:
int for_max(int n) //n为原字符串的长度 { for(int j=n;j>=0;j--) //字符填充 { s[j*2+2]=s[j]; s[j*2+1]='#'; } s[0]='$'; //防止数组越界填充第一个字符和最后一个字符为‘$’和‘/0’ int id = 0; //id为上述i int maxlen = 0; p[0] = 1; for(int j=2;j<2*n+2;j++) //j为上述i+k,所以k=j-id { if(p[id]+id<=j) p[j]=1; //第一种情况 else p[j]=min(p[2*id-j],p[id]-(j-id)); //第二种情况的两种小情况 while(s[j+p[j]]==s[j-p[j]]) p[j]++; //继续往后找 if(p[j]+j>p[id]+id) id=j; //更新最靠近字符串后面的点 maxlen=max(maxlen,p[j]-1); //更新最长回文 } return maxlen; }
具体题目可以看HDU3068:题意大概为给一个字符串,求最长回是多少
http://acm.hdu.edu.cn/showproblem.php?pid=3068
代码如下:
#include <iostream> #include <cstdio> #include <cstring> #include <queue> #include <algorithm> #include<vector> #pragma comment(linker,"/STACK:1024000000,1024000000") using namespace std; const int maxn = 2.2e5+5; char s[maxn*2]; //2倍数组 int p[maxn*2]; int for_max(int n) //n为原字符串的长度 { for(int j=n;j>=0;j--) //字符填充 { s[j*2+2]=s[j]; s[j*2+1]='#'; } s[0]='$'; //防止数组越界填充第一个字符和最后一个字符为‘$’和‘/0’ int id = 0; //id为上述i int maxlen = 0; p[0] = 1; for(int j=2;j<2*n+2;j++) //j为上述i+k,所以k=j-id { if(p[id]+id<=j) p[j]=1; //第一种情况 else p[j]=min(p[2*id-j],p[id]-(j-id)); //第二种情况的两种小情况 while(s[j+p[j]]==s[j-p[j]]) p[j]++; //继续往后找 if(p[j]+j>p[id]+id) id=j; //更新最靠近字符串后面的点 maxlen=max(maxlen,p[j]-1); //更新最长回文 } return maxlen; } int main() { int n; while(scanf("%s",s)!=EOF){ int m=strlen(s); printf("%d\n",for_max(m)); } }
版权声明:本文为博主原创文章,未经博主允许不得转载。