O(N)最長迴文子串演算法——Manacher演算法
題意:在一串連續的字串中尋找它的最長子串(Longest Palindromic Substring)
輸入:先從標準輸入讀取一個整數N(N<=30),代表字串的個數,接下來N行給出N個字串(字串長度<=10^6)
輸出:最長迴文子串長度
題目分析:很自然地想到這樣的演算法,長度為n的字串,它的最大回文子串長度為n,那麼按長度遞減的順序去原串中查詢,依次尋找是否有長度為n、n-1、n-2……的子串,一旦找到就跳出迴圈;迴文串的判斷則用一個函式實現,從兩端往中間比較。方法是對的,不過這個演算法的時間複雜度是O(N^3),題目的字串可能很長(10^6),肯定會超時。
上面的求解思路,我們容易發現,它實際上會進行很多重複的比較。假設字串為:
如何減少重複比較是提升演算法效率的關鍵。實際上,解決該問題有個很經典的演算法:Manacher演算法,下面來看它是如何減少重複比較次數的。
Manacher演算法判斷迴文串的方法和上述略微不同,它是從中心字元出發,向兩端移動比較,但是這樣只能解決長度為奇數的字串判斷,因為對於偶數長度,實際上並不存在所謂的中心字元。這裡有個巧妙的方法,將所有字串都轉化成奇數長度:在原串的每兩個字元之間都填上一個特殊字元(它不能存在於原串中,一般用‘
Manacher演算法從頭開始對每個字元計算以它為中心的最長迴文串長度,遍歷一次得到最長迴文串長度。當然如果老老實實對每個字元都從±1的位置開始比較,那麼演算法時間複雜度是O(N^2),Manacher演算法當然不是這麼做的。
定理1. 假設有迴文串S,其中心下標為md,則有S[md+i] = S[md-i], i≤S.length()/2.
推論1. 假設有迴文串S,其中心下標為md,i,j
(i < j)是關於md對稱的兩個下標,則由定理
推論2. 若用lps[]存放S中每個字元為中心的最長迴文串長度,由推論1,在S的範圍內,有lps[j]= lps[i],因為i = md-(j-md),也可以寫作lps[j] = lps[2*md - j].
推論3. 假設有迴文串S,其中心下標為md,i,j (i < j)是關於md對稱的兩個下標,則由推論2,有lps[j]= min{ lps[i], mx - j }或lps[j]= min{lps[2*md - j], mx - j},其中mx為S的右端。
下圖解釋了推論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#計算第一個a的lps值時,會越過0的陣列邊界。所以在開頭加'$'充當“哨兵”,這樣就免去了在while迴圈中判斷越界,另外在字串的末尾,有'\0'保證不會越界,如果同時到達了字串的開頭和末尾,因為'#'!='$',所以也不會越界。