1. 程式人生 > >O(N)最長迴文子串演算法——Manacher演算法

O(N)最長迴文子串演算法——Manacher演算法

題意:在一串連續的字串中尋找它的最長子串(Longest Palindromic Substring)

輸入:先從標準輸入讀取一個整數NN<=30),代表字串的個數,接下來N行給出N個字串(字串長度<=10^6)

輸出:最長迴文子串長度

題目分析:很自然地想到這樣的演算法,長度為n的字串,它的最大回文子串長度為n,那麼按長度遞減的順序去原串中查詢,依次尋找是否有長度為nn-1n-2……的子串,一旦找到就跳出迴圈;迴文串的判斷則用一個函式實現,從兩端往中間比較。方法是對的,不過這個演算法的時間複雜度是O(N^3),題目的字串可能很長(10^6),肯定會超時。

上面的求解思路,我們容易發現,它實際上會進行很多重複的比較。假設字串為:

ACABA,在判斷長度為5的子串是否有迴文串的時候,比較了CB,在判斷長度為3的子串時,再次比較了CB。當字串很長的時候,這種重複的比較將普遍存在,如果長度為n的字串中每個字元都不相同(即最長迴文子串的長度為1),則長度為k的迴文串判斷,進行的比較次數為⌊k/2⌋n中一共包含n-k+1個長度為k的子串,則進行 (n-k+1)*⌊k/2⌋次比較,所以從n~ 2一共進行的比較次數為:


如何減少重複比較是提升演算法效率的關鍵。實際上,解決該問題有個很經典的演算法:Manacher演算法,下面來看它是如何減少重複比較次數的。

Manacher演算法判斷迴文串的方法和上述略微不同,它是從中心字元出發,向兩端移動比較,但是這樣只能解決長度為奇數的字串判斷,因為對於偶數長度,實際上並不存在所謂的中心字元。這裡有個巧妙的方法,將所有字串都轉化成奇數長度:在原串的每兩個字元之間都填上一個特殊字元(它不能存在於原串中,一般用‘

#’作為特殊字元),同樣在頭和尾也補充該字元,所以字串長度變成2*N+1,字串的形式為:#C#C#C#C#C表示原串的字元)。

       Manacher演算法從頭開始對每個字元計算以它為中心的最長迴文串長度,遍歷一次得到最長迴文串長度。當然如果老老實實對每個字元都從±1的位置開始比較,那麼演算法時間複雜度是O(N^2)Manacher演算法當然不是這麼做的。

定理1. 假設有迴文串S,其中心下標為md,則有S[md+i] = S[md-i], iS.length()/2.

推論1. 假設有迴文串S,其中心下標為mdi,j (i < j)是關於md對稱的兩個下標,則由定理

1S[i+k]= S[j-k], S[i-k] = S[j+k], 其中i,jk的加減法不超過S的範圍。

推論2. 若用lps[]存放S中每個字元為中心的最長迴文串長度,由推論1S的範圍內,有lps[j]= lps[i],因為i = md-(j-md),也可以寫作lps[j] = lps[2*md - j].

推論3. 假設有迴文串S,其中心下標為mdi,j (i < j)是關於md對稱的兩個下標,則由推論2,有lps[j]= min{ lps[i], mx - j }lps[j]= min{lps[2*md - j], mx - j},其中mxS的右端。

下圖解釋了推論3中的min操作,假如lps[i]沒有超過S的邊界,那麼lps[j] = lps[i];假如lps[i]超過或恰好到達S的邊界,那麼超過的部分,lps[i]無法成為lps[j]的保證,從越過邊界(mx)的位置開始,lps[j]必須往兩端一一比較,lps[j]=mx - j(這裡有個等價關係,lps[]既表示去掉#以後最長迴文串長度,也表示#存在時單邊的長度)。推論3中的這條語句,是Manacher演算法的核心,理解了它也就理解了Manacher演算法。


下面給出完整的程式碼:

#include <bits/stdc++.h>
using namespace std;

char str[2000005];
int  lps[2000005];
int Manacher(string s)//manacher algorithm
{
	int length = s.size(), j = 2;
	str[0] = '$'; str[1] = '#';
	//插入#
	for(int i = 0; i < length; i++)//$#c#c#c#'\0'
	{
		str[j++] = s[i];
		str[j++] = '#';
	}
	str[j] = '\0';
	length = (length << 1) + 2;
	
	lps[0] = 1;
	int mx = 0, md = 0, max_len = 0;//當前迴文串能達到的最右端,及其中心
	for(int i = 1; i<length; i++)
	{
		if(i >= mx)  lps[i] = 1;
		else  lps[i] = min(lps[2*md-i], mx-i);
		while(str[i-lps[i]] == str[i+lps[i]])
			lps[i]++;

		if(i+lps[i] > mx)
		{
			mx = i+lps[i];
			md = i;
		}
		if(lps[i] > max_len)  
			max_len = lps[i];
	}
	printf("%d\n", max_len-1);
}
int main()
{
	int n;  
	string s;
	cin >> n;
	while(n--)
	{
		cin >> s;
		Manacher(s);
	}
	return 0;
}

上述程式碼的實現和之前討論略有不同,一個處理細節是在開頭加上了'$',這是因為字串結尾為'\0',所以需要在頭部加一個字元維持奇數長度;另外,如果加'#',那麼在字串#a#b#a#c#d#a#計算第一個alps值時,會越過0的陣列邊界。所以在開頭加'$'充當“哨兵”,這樣就免去了在while迴圈中判斷越界,另外在字串的末尾,有'\0'保證不會越界,如果同時到達了字串的開頭和末尾,因為'#'!='$',所以也不會越界。