1. 程式人生 > >【字串專題】(一) Manacher演算法

【字串專題】(一) Manacher演算法

迴文子串         迴文分為奇數迴文和偶數迴文,即字串的長度為奇數還是偶數。其實只需要討論奇數的情況,即以某個字元為中心,左右兩邊的字元按照中心對稱的情況(偶數的情況可以通過本文末尾的方法轉換成奇數的情況)。
最長迴文子串         樸素演算法         列舉每個字元,作為中心,向兩邊擴充套件判斷是否相等,找到最大的可行長度。演算法最壞複雜度O(n^2)。
圖1          Manacher演算法         用p[i]表示以第i個字元為中心軸,兩邊字元軸對稱的最大半徑。如圖所示,p[1]=1,p[2]=2,p[4]=3,p[7]=5。主體思想還是和樸素演算法類似,從小到大列舉i,計算p[i]。核心思想在於p[i]的初始值,它不再是1,而是根據之前計算的{p[j] | 1<=j<i}來確定p[i]的初始值。
圖2         我們用一個變數ct來儲存前面所有字元產生的最大回文子串的“最大右邊界”的中心軸位置,“最大右邊界”的含義不是p[i]最大,而是i + p[i]最大,如圖2中列舉完前8個位置後,得到ct=7,對應的右邊界為ct + p[ct] - 1。         然後,我們有了一些初始資訊,那麼當列舉到第i個字元時,分情況討論:
        1)如圖3所示,如果i<ct+p[ct],即第i個字元必然落在以ct為中心軸的最大回文子串之內。那麼我們必然可以找到一個和i點按照ct軸對稱的點j,滿足(i+j)/2=ct,則j = 2*ct-i。 圖3         這時候,我們繼續分情況討論,j的位置可能衍生出三種情況,如圖4所示:
圖4         (a)的情況,p[i]=p[j],而且不可能再長。假設p[i]能夠再長,則根據對稱性,p[j]也能夠再長,但是p[j]已經確定了,所以假設不成立;         (b)的情況,p[i]=p[j],和(a)不同的是,它有可能還可以向兩邊擴張;         (c)的情況,p[i]=ct+p[ct]-i,而且不可能再長。還是利用反證法,如果p[i]兩邊可以再擴張一個字元,如圖中的y和z部分相等,那麼必然x和y部分也相等(根據對稱性),而x和w也相等,從而w和z相等,這樣說明原本的p[ct]可以再增長,而p[ct]已經確定了,所以假設不成立。 圖5         2)i>=ct+p[ct],那麼前面的一些計算資訊無法為我們所用,於是p[i] = 1; 圖6         綜合上述情況,可以得到p[i]的初始值如下:
if (i < ct + p[ct]) {
    p[i] = min(p[2*ct-i], ct + p[ct]-i);
}else {
    p[i] = 1;
}


        然後再迴圈判斷第i-p[i]個字元和第i+p[i]個字元是否相等,如果相等則p[i]++;否則退出迴圈。然後利用新的i+p[i]更新ct。
程式碼實現
int Manacher(char *str) {
	int ct = 0, r = 0, maxLen = 1;
	p[0] = 1;
	for(int i = 1; str[i]; ++i) {
		// 1.計算p[i]初始值 
		if(i < r) {
			p[i] = Min(p[2*ct-i], r-i);
		}else {
			p[i] = 1;
		}
		// 2.擴張p[i],以適應達到p[i]最大值 
		while(i-p[i]>=0 && str[i-p[i]] == str[i+p[i]])
			++p[i];
		// 3.更新ct
		if(p[i] + i > r) {
			ct = i;
			r = p[i] + i;
		}
		// 4.更新最長迴文 
		if(2*p[i]-1 > maxLen) {
			maxLen = 2*p[i] - 1;
		}
	}
	return maxLen;
}
     當所有字元都相等時,該演算法達到最壞複雜度,觀察可得複雜度O(n)。
奇偶迴文轉換         以上演算法只支援字串長度為奇數的情況,那麼接下來我們通過一種方法,將偶數字符串轉換成奇數。如圖所示,相鄰字元間插入一個不在該字串的字符集中的字元,比如$。原串長度為s,則轉換後的串長度為2*s+1,所以必然為奇數。 圖7         並且這麼做以後有幾個比較明顯的性質: 1、任何一個迴文子串的兩端必然是'$'。 2、以'$'為中心的最長迴文子串的半徑一定是奇數;反之,一定是偶數。

HDU 3068 最長迴文 Manacher
HDU 3294 Girl's research Manacher
HDU 4513 吉哥系列故事——完美隊形II M anacher+ 單調性
HDU 5677 ztr loves substring Manacher+動態規劃
HDU 5340 Three Palindromes Manacher+列舉
HDU 5785 Interesting Manacher+樹狀陣列
HDU 5371 Hotaru's problem Manacher+RMQ+列舉

HDU 3068 最長迴文         題意:給定一個長度為N(N<=110000)的字串,求它的最長迴文子串的長度。 題解:間隔插入字元'$'後,利用Manacher演算法求出每個字元為中心的最長迴文子串半徑p[i],比較取得最大的p[i]記為Max,則Max-1就是最後的答案。
HDU 3294 Girl's research         題意:在一個長度為N(N<=200000)的字串中,找到一個最長迴文子串,並且輸出。 題解:間隔插入字元'$'後,利用Manacher演算法求出每個字元為中心的最長迴文子串半徑p[i],比較取得最大的p[i],輸出兩端即可。

HDU 4513 吉哥系列故事——完美隊形II         題意:在一個長度為N(N<=100000)的整數序列中,找到一個連續的迴文序列,且從左到中間保持單調不降。
題解:對於原序列,計算一個輔助陣列h,h[i]代表從自己到最左邊的單調不增的序列長度。然後在原序列間隔插入-1, 利用Manacher演算法求出每個數為中心的最長迴文子序列的半徑p[i],然後對新序列(插入-1的序列)的每個i (i 下標從0開始計) 進行計算求最大值。迴文和單調取交集,即滿足半徑小的那個。 以第i個元素為中心軸,如果這個元素是-1,則兩邊必然偶對稱,min{p[i]-1, 2*h[i/2-1]};否則為奇對稱,取值為min{p[i]-1, 2*q[i/2]-1},取所有這些串中的最大值就是答案。 圖8

HDU 5677 ztr loves substring        題意:給定N(N<=100)個字串,要求取出其中K(K<=100)個子串,組成一個長度為L(L <=100)的子串。並且這些子串需要滿足的條件是必須迴文。可行輸出Yes,不可行輸出No。
      題解:利用Manacher求出每個串的迴文子串中長度為 i (i <= 該字串長度)的個數。然後將這些個數合併。       用dp[i][j][k]表示可以選擇的子串長度為1-i的情況下,選擇個數為j個,組成長度為k的方案是否可行。然後就是一個O(n)的狀態轉移,演算法最壞複雜度O(n^4)。實際上肯定達不到,因為只要有一個i滿足dp[i][K][L]=true,則可以直接輸出Yes了。       初始狀態dp[0][0][0]=true,其它均為false,然後列舉進行狀態轉移即可。
HDU 5340 Three Palindromes         題意:給定N(N<=20000)個字串,求判斷是否能夠將它切兩刀,將它變成三個迴文串。       題解:首先還是插入字元'$',求一遍Manacher,然後開始找一些有趣的性質。我們可以分成兩種情況討論:       1)第一種,中間那個串的最長迴文子串去掉後,兩邊的串分別正好是迴文串。這種情況比較簡單,列舉每個字元作為中心軸,去掉中間那個迴文串後,確定兩邊兩個串的中心軸,利用p[i]判斷是否滿足迴文條件即可。 圖9
      2)第二種,中間那個串的最長迴文去掉後,兩邊無法再構成迴文;但是中間串縮短長度後,兩邊有可能構成迴文。如圖所示: 圖10 這種情況下,我們需要列舉中間那個串的實際長度,這個實際長度最大值為p[i],然後每次減2。由於逐漸減小長度的過程中,必然兩個邊界是按照中心軸對稱的,而邊界需要滿足分別和原串的兩邊界對稱,所以在減小長度的同時可以比較原串的收尾是否有不相等的情況,一旦不相等就可以退出迴圈了,複雜度小於O(n^2)。
HDU 5785 Interesting(推薦)         題意:給定一個長度小於等於N(N<=10^6)的字串S[N],有三元組(i,j,k)滿足1<=i<=j<k<len,且S[i...j]和S[j+1...k]均為迴文串。現在需要輸出所有這些三元組的i*k的和模1000000007
       題解:首先插入字元'$',原串S變成S',然後求一遍Manacher,記錄下p[i]。然後就是列舉j,看左邊有多少i滿足S[i...j]是迴文,右邊有多少k滿足S[j+1...k]是迴文。如果用樸素的做法,統計所有i的和以及k的和相乘加和,總的複雜度為O(n^2)。所以我們可以採用樹狀陣列來加速這部分求和。       我們先討論S[i...j]部分的求和統計。目的是找到以j結尾的所有迴文串S[i...j]中i的和。       首先列舉所有非'$'的字元y,然後我們的目的是要找到所有的x<=y,滿足g=(x+y)/2,且g+p[g]>y。由於y對應的字元不是'$',根據對稱性,x對應的字元也不是'$'。x字元對應原字串的下標為(x+1)/2。       所以問題轉化成求: 圖11 圖12         例如,這個串以第9個字元結尾的迴文串的左端點x集合是{1,5,9},經過座標轉換後,原串的所有i求和為sum{ (x+1)/2 | x=1,5,9 }=1+3+5=9。
圖13
      然而,實際計算的時候,所有i的求和只能通過O(n)的遍歷,這樣總的複雜度會變成O(n^2),為了能夠用樹狀陣列進行統計,我們引入了一箇中間變數,即所有這些迴文串中心g。       因為g=(x+y)/2,所以x = 2g-y,於是有: 圖14       sum{g}代表所有滿足條件的迴文的中心軸數值的和,cnt代表有多少個迴文串。那麼,我們可以用兩個樹狀陣列分別統計這兩個值,如圖所示:
圖15       維護兩個樹狀陣列,一個統計cnt,一個統計sum。計算cnt的時候,對於每個p[g],在g位置+1,g+p[g]位置-1;計算sum的時候,對於每個p[g],在g位置+g,在g+p[g]位置-g。       這樣對於每個j,就能計算所有i的和了,用同樣方法計算所有k的和,然後相乘取模即可。演算法複雜度O(nlogn)。當然還有一個更加高效的演算法,每次插入和計算求和的時候都是遞增的進行,所以可以不需要樹狀陣列,直接一個O(n)的陣列維護字首和即可,總演算法複雜度可以達到O(n)。
HDU 5371 Hotaru's problem         題意:給定一個N(N<=10^5)個元素的陣列。定義N-序列為3K個元素,前K個和中間K個完全對稱,並且前K個和最後K個元素完全相等。求N個元素中最長的N-序列的長度。       題解:由題意得,前K個和中間K個構成偶迴文,且中間K個和最後K個也構成偶迴文。 首先,資料量大的情況下,用G++提交利用輸入外掛,可以節省500MS時間。然後就是各種亂搞,在相鄰元素中間插入-1後跑一邊Manacher,然後就是列舉以每個-1為對稱軸的迴文串,記錄最優解Max。 圖16 列舉每個-1的對稱軸i,然後在p[i]覆蓋範圍內找右邊串的對稱軸j,演算法複雜度O(n^2)。但是,這裡可以做很多優化,比如在當前可能的軸i和軸j之間如果沒有一個k滿足k-p[k]+1<=i,則不需要列舉j直接跳出迴圈。這一步可以用RMQ來完成。如果當前情況下能夠求得的最優解已經小於等於Max,也不需要繼續往下列舉。 還有一個比較依賴資料的剪枝,就是3*(Max+1)>N的時候退出整個迴圈(用於所有元素均相等的情況)。