Manacher算法之个人愚见

来源:互联网 发布:阿里云 画图 工具 编辑:程序博客网 时间:2024/05/17 04:11

这个俗称“马拉车”的算法的适用问题比较局限,是Manacher在解决寻找字符串中最长回文时提出的一种时间复杂度为O(n)的算法。废话不多说,直接讲算法:

字符串预处理

首先,在解决回文类型的算法题目时,经常会很头疼的问题就是最终解的回文中字符总数是奇数还是偶数,即abba与aba两种类型,比如判断一个字符串是不是回文就要区别对待这两种情况。但在Manacher算法中首先针对这个问题提出了解决办法,即在字符串的开头及每一个字符后加上一个固定的字符“#”(其实,一直有问题困扰我的就是如果字符值域涵盖了整个字符集该如何解决?当然,不要在意这些细节)。

比如,字符串:

12141214131(4为回文中心)

121441214131(44为回文中心)

在经过加处理后分别为:

#1#2#1#4#1#2#1#4#1#3#1#(4为回文中心)

#1#2#1#4#4#1#2#1#4#1#3#1#(#为回文中心)

可以看出,经过处理后的奇偶不同的问题就解决了,剩下的问题就是如何在处理后的字符串中找到最长回文。

寻找最长回文

就如何寻找最长回文这个问题,首先,先介绍几个变量:

S[i] : 当前字符。

P[i] : 在以当前字符S[i] 为回文中心的回文半径长度。

Final_Index : 简称(fi),这个变量代表当前找到的所有回文中最右端的一个字符索引。

Center_Index : 简称(ci)这个变量表示的就是能到达fi索引的回文的中心索引。

以上变量符合等式:fi = ci + P[ci],即:当前能达到的最右端的索引fi等于以fi为结尾的回文的中心S[ci]的索引ci加上其回文半径P[ci]。没看懂?上例子: #1#2#1#4#1#2#1#4#1#3#1#,当前考虑S[7] ,也就是字符4,以该字符为中心的回文是#1#2#1#4#1#2#1#,是当前能达到最右端的索引的回文(注意最右端,并不是最长),所以P[7] = 7(不含中心本身),此时fi = ci + P[ci] = 7 + P[7]=14,此时将ci置为 7 。当计算到S[11]时,此时回文达到的最右端为18 > 14,此时需要更新ci = 11,fi = 18。如果还没明白就看图吧。

计算完P[7]后的ci,fi位置。

 

计算完P[11]后的ci,fi位置。

 

明白这些变量的含义与等式关系后,接下来就是算法的最核心的部分,如何利用已经计算过的P[0]……P[ci]...来求出P[i];但是得到P[i]的方法要根据不同的情况分开来考虑,i 与 fi的大小是分情况讨论的条件。

(一)首先看i > fi的情况

假设我们当前要计算下图中绿色3字符为中心的回文半径即P[19]。而根据之前的计算fi = 18,ci = 11,i=19 >fi,说明目前还没有一个回文包括S[19],所以计算P[19]只能靠简单前后遍历得到。

 j= 0 ;

while(S[19+j]==S[19-j])j++ ;

P[19] = j ;

 

(二)再来看i < fi 的情况,这种情况下还有三种不同子情况,下面分开来说:

(1)假设现在要计算P[9],此时9 < fi,说明S[9]已经存在于一个回文中,而这个回文我们记录下了即:ci = 7代表中心,P[ci] = 7 代表半径的回文中(图中蓝色双箭头范围),根据回文的对称性,P[9] 可以参见 P[ci –(9-ci)] 即P[5] 的情况,因为 5,9以7为中心对称,虽然可以参见P[5] ,但是不能断定P[9] 等于 P[5],具体情况还要看P[5],P[5] = 1可以看出,以S[5]为中心的回文(图中橙色双箭头范围)完全包含在以S[7]为中心的回文中,且没有到达S[7]为中心的回文的左边界,此时可以断定P[9] = P[5];那么有一个疑问,有没有可能P[9]>P[5]或P[9]<P[5]呢,答案是不可能,为什么?同样是因为对称性,假设P[9] = 4,那么P[5]也等于4,与前面的计算P[5] = 1相矛盾,但是请注意这种情况的前提,即红字部分 :  S[5]为中心的回文完全包含在以S[7]为中心的回文中,且没有到达S[7]为中心的回文的左边界。重要的事情说三遍:以S[5]为中心的回文完全包含在以S[7]为中心的回文中,且没有到达S[7]为中心的回文的左边界。

 

(2)假设现在要计算P[11],同样的 11 < fi,S[11]存在于S[ci = 7]为中心,P[ci] = 7 代表半径的回文中,根据回文的对称性,P[11] 要参看P[3],而P[3] = 3,此时出现了以S[3]为中心的回文(下图橙色双箭头范围)的左边界跟以S[ci=4]为中心的回文(下图蓝色双箭头)的左边界重合的情况,根据对称性,S[11]的回文右边界一定能达到以S[ci=4]为中心的回文(下图绿色双箭头范围)的右边界即fi。由第一种情况可以推知P[11]是不小于P[3]的,但有没有可能大于P[3]呢?如下图,可以看出来,根据第一种情况首先可以确定以S[11]为中心的回文是不小于绿色双箭头所指范围的,实际情况是绿色箭头范围的两边延伸也有属于S[11]为中心的回文即黄色范围,因此在计算P[11]时,还需要遍历一下绿色范围的前后。

 

        (3)再来说第三种情况,我们在第二种情况下修改一下例子来讨论第三种情况,假设例子中字符串的前面还有若干字符串有N个。同样的假设现在要计算P[N+11],N+11 < N+fi,S[N+11]存在于S[ci = N+7]为中心,P[ci] = 7 代表半径的回文中,根据回文的对称性,P[N+11] 要参看P[N+3],而此时我们假设P[N+3] = 5。可以看出此时橙色回文的左边界已经超过了蓝色回文的左边界,此时的P[N+11]又要如何计算呢?其实这种情况的计算方法和第二种情况相同,我们首先看图中红色范围中的回文,这部分回文既在橙色回文中也在蓝色回文中,且根据对称性可知,图中绿色回文跟红色回文是相同的,因为红色回文左边界跟蓝色回文的左边界重合,所以绿色回文的右边界跟蓝色回文的右边界重合,所以可以肯定的是P[N+11]>=N+fi –(N+11),其中N+fi-(N+11)就是计算S[N+11]S[N+fi]的距离。而在绿色回文的延伸部分中是否也有回文就要看前后遍历的结果了。

 

其实第2 、3两种子情况可以合起来算作一种即:当橙色回文的左边界小于等于蓝色回文左边界的时候,所求绿色回文的右边界就一定能达到蓝色回文的右边界,至于有没有延伸就要看前后遍历的结果了。


代码实现

const int MAX_SIZE=10000000+10;  char str[MAX_SIZE];//原字符串  char tmp[MAX_SIZE<<1];//预处理后的字符串  int Len[MAX_SIZE<<1];  //预处理原始串  int PreDo(char *str)  {      int i;    int len=strlen(str);      tmp[0]='¥';  //字符串加一个字符,防止越界    for(i=1;i<=2*len;i+=2)      {          tmp[i]='#';          tmp[i+1]=st[i/2];      }      tmp[2*len+1]='#';      tmp[2*len+2]='@';//字符串结尾加一个字符,防止越界     return 2*len+1;//返回转换字符串的长度,不包括开头结尾加上的字符}  //Manacher算法计算过程  int Manacher(char *tmp,int len)  {       int fi=0,ret=0,ci=0;//fi记录当前所有回文达到的字符串的最右字符的索引,ci以fi为右边界的回文中心索引       for(int i=1;i<=len;i++)       {           if(fi>i)            Len[i]=min(fi-i,Len[2*ci-i]);//在Len[j](i关于ci的对称点)和fi-i中取较小值         else         Len[i]=0;//如果i>=fi,开始左右延伸遍历。          while(tmp[i-Len[i]]==tmp[i+Len[i]])            Len[i]++;           if(Len[i]+i>fi)//若新计算的回文串右边界索引大于fi,要更新ci,fi的值           {               fi=Len[i]+i;               ci=i;           }           ret=max(ret,Len[i]);       }       return ret;  }


0 0
原创粉丝点击