標籤:
前幾天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)); }}
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
演算法進階之manacher演算法 (求最長迴文)