求最長回文子串,O(n)復雜度
最長回文子串問題—Manacher算法
最長回文串問題是一個經典的算法題。
0. 問題定義
最長回文子串問題:給定一個字符串,求它的最長回文子串長度。
假設一個字符串正著讀和反著讀是一樣的,那它就是回文串。以下是一些回文串的實例:
12321 a aba abba aaaa tattarrattat(牛津英語詞典中最長的回文單詞)
1. Brute-force解法
對於最長回文子串問題,最簡單粗暴的辦法是:找到字符串的全部子串,遍歷每一個子串以驗證它們是否為回文串。
一個子串由子串的起點和終點確定,因此對於一個長度為n的字符串,共同擁有n^2個子串。這些子串的平均長度大約是n/2。因此這個解法的時間復雜度是O(n^3)。
2. 改進的方法
顯然全部的回文串都是對稱的。長度為奇數回文串以最中間字符的位置為對稱軸左右對稱,而長度為偶數的回文串的對稱軸在中間兩個字符之間的空隙。可否利用這樣的對稱性來提高算法效率呢?答案是肯定的。我們知道整個字符串中的全部字符,以及字符間的空隙。都可能是某個回文子串的對稱軸位置。能夠遍歷這些位置,在每一個位置上同一時候向左和向右擴展,直到左右兩邊的字符不同。或者達到邊界。對於一個長度為n的字符串,這樣的位置一共同擁有n+n-1=2n-1個。在每一個位置上平均大約要進行n/4次字符比較,於是此算法的時間復雜度是O(n^2)。
3. Manacher 算法
對於一個比較長的字符串,O(n^2)的時間復雜度是難以接受的。
Can we do better?
先來看看解法2存在的缺陷。
1)因為回文串長度的奇偶性造成了不同性質的對稱軸位置。解法2要對兩種情況分別處理。
2)非常多子串被反復多次訪問。造成較差的時間效率。
缺陷2)能夠通過這個直觀的小??體現:
char: a b a b a
i : 0 1 2 3 4
當i==1。和i==2時,左邊的子串aba分別被遍歷了一次。
假設我們能改善解法2的不足,就非常有希望能提高算法的效率。
Manacher正是針對這些問題改進算法。
(1) 解決長度奇偶性帶來的對稱軸位置問題。
Manacher算法首先對字符串做一個預處理。在全部的空隙位置(包含首尾)插入相同的符號。要求這個符號是不會在原串中出現的。這樣會使得全部的串都是奇數長度的。
以插入#號為例:
aba ---> #a#b#a#
abba ---> #a#b#b#a#
插入的是相同的符號,且符號不存在於原串,因此子串的回文性不受影響。原來是回文的串,插完之後還是回文的,原來不是回文的,依舊不會是回文。
(2)解決反復訪問的問題。
我們把一個回文串中最左或最右位置的字符與其對稱軸的距離稱為回文半徑。Manacher定義了一個回文半徑數組RL,用RL[i]表示以第i個字符為對稱軸的回文串的回文半徑。
我們一般對字符串從左往右處理,因此這裏定義RL[i]為第i個字符為對稱軸的回文串的最右一個字符與字符i的距離。
對於上面插入分隔符之後的兩個串,能夠得到RL數組:
char: # a # b # a #
RL : 1 2 1 4 1 2 1
RL-1: 0 1 0 3 0 1 0
i : 0 1 2 3 4 5 6
char: # a # b # b # a #
RL : 1 2 1 2 5 2 1 2 1
RL-1: 0 1 0 1 4 1 0 1 0
i : 0 1 2 3 4 5 6 7 8
上面我們還求了一下RL[i]-1。
通過觀察能夠發現。RL[i]-1的值。正是在原本那個沒有插入過分隔符的串中,以位置i為對稱軸的最長回文串的長度。那麽僅僅要我們求出了RL數組,就能得到最長回文子串的長度。
於是問題變成了,如何高效地求的RL數組。
基本思路是利用回文串的對稱性,擴展回文串。
我們再引入一個輔助變量MaxRight
。表示當前訪問到的全部回文子串,所能觸及的最右一個字符的位置。另外還要記錄下MaxRight
相應的回文串的對稱軸所在的位置。記為pos
。它們的位置關系例如以下。
我們從左往右地訪問字符串來求RL,假設當前訪問到的位置為i
,即要求RL[i],在相應上圖,i
必定是在po
右邊的(obviously)。
但我們更關註的是。i
是在MaxRight
的左邊還是右邊。我們分情況來討論。
1)當i
在MaxRight
的左邊;
情況1)能夠用下圖來刻畫:
我們知道,圖中兩個紅色塊之間(包含紅色塊)的串是回文的;而且以i
為對稱軸的回文串,是與紅色塊間的回文串有所重疊的。我們找到i
關於pos
的對稱位置j
。這個j
相應的RL[j]
我們是已經算過的。
依據回文串的對稱性,以i
為對稱軸的回文串和以j
為對稱軸的回文串,有一部分是相同的。這裏又有兩種細分的情況。
a. 以j
為對稱軸的回文串比較短,短到像下圖這樣。
這時我們知道RL[i]至少不會小於RL[j],而且已經知道了部分的以i
為中心的回文串,於是能夠令RL[i]=RL[j]
。可是以i
為對稱軸的回文串可能實際上更長,因此我們試著以i
為對稱軸,繼續往左右兩邊擴展,直到左右兩邊字符不同,或者到達邊界。
b. 以j
為對稱軸的回文串非常長,這麽長:
這時,我們僅僅能確定,兩條藍線之間的部分(即不超過MaxRight的部分)是回文的。於是從這個長度開始,嘗試以i
為中心向左右兩邊擴展,。直到左右兩邊字符不同,或者到達邊界。
不論以上哪種情況,之後都要嘗試更新MaxRight
和pos
,因為有可能得到更大的MaxRight。
詳細操作例如以下:
step 1: RL[i] <--- min(RL[2*pos-i], MaxRight-i)
step 2: 以i為中心擴展回文串,直到左右兩邊字符不同。或者到達邊界。
step 3: 更新MaxRight和pos
2)當i
在MaxRight
的右邊。
遇到這樣的情況,說明以i
為對稱軸的回文串還沒有不論什麽一個部分被訪問過。於是僅僅能從i
的左右兩邊開始嘗試擴展了,當左右兩邊字符不同,或者到達字符串邊界時停止。然後更新MaxRight
和pos
。
(3)算法實現
def manacher(s):
#預處理
s=‘#‘+‘#‘.join(s)+‘#‘
RL=[0]*len(s)
MaxRight=0
pos=0
MaxLen=0
for i in range(len(s)):
if i<MaxRight:
RL[i]=min(RL[2*pos-i], MaxRight-i)
else:
RL[i]=1
#嘗試擴展,註意處理邊界
while i-RL[i]>=0 and i+RL[i]<len(s) and s[i-RL[i]]==s[i+RL[i]]:
RL[i]+=1
#更新MaxRight,pos
if RL[i]+i-1>MaxRight:
MaxRight=RL[i]+i-1
pos=i
#更新最長回文串的長度
MaxLen=max(MaxLen, RL[i])
return MaxLen-1
(4)復雜度分析
空間復雜度:插入分隔符形成新串。占用了線性的空間大小。RL數組也占用線性大小的空間,因此空間復雜度是線性的。
時間復雜度:雖然代碼裏面有兩層循環,通過amortized analysis我們能夠得出,Manacher的時間復雜度是線性的。因為內層的循環僅僅對尚未匹配的部分進行。因此對於每一個字符而言,僅僅會進行一次,因此時間復雜度是O(n)。
4. 很多其它關於回文串的fun facts(主要來自維基百科)
4.1 人們在一座名為赫庫蘭尼姆的古城遺跡中。找到了一個好玩的拉丁語回文串:sator arepo tenet opera
rotas
。翻譯成中文大概就是`一個叫做Arepo的播種者。他用力地扶(把)著車輪。
這個串的每一個單詞首字母剛好組成了第一個單詞,每一個單詞的第二個字母剛好組成了第二個單詞...於是乎,假設寫出醬紫,你會發現上下左右四個方向讀起來是一樣的。這個串被稱為 Sator Square.
4.2 本文開頭給出的單詞tattarrattat
。出如今愛爾蘭作家詹姆斯·喬伊斯的小說《尤利西斯》,是敲門的意思。吉尼斯紀錄的最長回文英文單詞是detartrated
,是個化學術語。另外。還有些已出版的英文回文小說(你們歪果仁真會玩),比方Satire:
Veritas,Dr Awkward & Olson in Oslo等。
能夠採用動態規劃,列舉回文串的起點或者終點來解最長回文串問題。無需討論串長度的奇偶性。
看以下的代碼。easy理解。
public int longestPalindrome(String s) {
int n=s.length();
boolean[][] pal=new boolean[n][n];
//pal[i][j] 表示s[i...j]是否是回文串
int maxLen=0;
for (int i=0;i<n;i++){ // i作為終點
int j=i; //j作為起點
while (j>=0){
if (s.charAt(j)==s.charAt(i)&&(i-j<2||pal[j+1][i-1])){
pal[j][i]=true;
maxLen=Math.max(maxLen, i-j+1);
}
j--;
}
}
return maxLen;
}
求最長回文子串,O(n)復雜度