問題定義
最長回文子串問題:給定一個字符串,求它的最長回文子串長度。
解法1:暴力解法
找到字符串的所有子串,判斷每一個子串是否是回文串。
一個子串有該串的起點和終點確定,因此對于長度為N的字符串共有 N2個子串, 這些子串的平均長度為N/2 (判斷是否是回文串的時間復雜度O(N)). 因此時間復雜度O(N3)
解法2:改進暴力解法
所有的回文串都是關于某個位置對稱。
長度為奇數回文串以最中間字符的位置為對稱軸左右對稱;
長度為偶數的回文串以中間兩個字符之間的空隙為對稱軸左右對稱。
我們可以遍歷字符串中的這些位置, 從每個位置同時向左右擴展,直到兩邊的字符不同,或達到邊界。這類位置共 N + N-1 = 2N-1個,且在每個位置上大約要進行N/4次字符比較,因此算法復雜度O(N2)
解法3:Mancher算法
解法2存在的缺陷:
- 存在很多子串被多次重復訪問比較的情況
- 回文串的長度的奇偶性,造成不同的對稱軸位置,解法2需要分別處理
步驟1:解決因回文串的長度的奇偶性需要分別處理對稱軸的問題
方法:在字符串的開頭和末尾,以及每兩個字符的中間位置插入唯一標識符#.
這樣構造字符串后,字符串的長度始終是奇數。 例如,
aba ==> #a#b#a#
abba ==> #a#b#b#a#
步驟2:解決子串多次重復訪問的問題
為了最大程度的利用已經訪問過的回文串的信息, Manacher算法巧妙的定義了回文半徑: 即,回文串最左或最右位置到對稱軸的距離。
回文半徑數組RL, RL[i]表示以第 i 個字符為對稱軸的回文串的回文半徑。例如,
RL[i]值的性質:RL[i]-1 表示 原始字符串中以 第 i 個位置為對稱軸 的最長回文串長度。
證明:
在改造過的字符串中,以 第 i 個位置為對稱軸的回文串的 最左和最右字符一定是 #.
第i個位置對應的字符,分兩種情況,(如圖1):
- 第i個位置對應的字符是 #,則回文串共有奇數個字符,從回文串的最左位置到第i個位置共有,(RL[i]-1)/2 個非#字符, (RL[i] - (RL[i]-1)/2)個#字符, 由于左右關于第 i 個位置對稱,因此,該回文串中非#字符共有 (2 * (RL[i]-1)/2) = (RL[i]-1)個非#字符。
- 第i個位置對應的字符是 非#字符,則回文串共有偶數個字符,從回文串的最左位置到第i個位置共有,RL[i]/2-1 個非#字符 (減1是為了不計算第 i 個位置的字符), (RL[i] - RL[i]/2+1)個#字符, 由于左右關于第 i 個位置對稱,因此,該回文串中非#字符共有 (2 * (RL[i]/2-1)) + 1 = (RL[i] - 1)個非#字符 (最后的+1,是將第 i 個位置也算上)。
步驟3:如何利用RL數組,減少重復訪問字符串
為了盡可能的減少重復訪問字符串的次數,引入變量 MaxRigth 表示 在從左到右,已經訪問過的回文串中,回文串所能觸及到字符串的最右位置,即該回文串的中心為pos,則其關系如下圖2所示:
idx在4和12之間的所有字符都關于pos位置對稱!
由于pos是已經訪問過的位置,則 當前訪問到的位置 i 只能位于pos的右邊,且有兩種情況:
1)當前訪問的位置 i 在MaxRight的左邊,如圖3所示
從圖中可以看出,以位置 i 為中心的回文串必然與以pos為中心的回文串存在一部分的重合。
現在我們想找出以位置 i 為中心的回文字符,為了減少重復訪問字符,我們希望可以知道以位置 i 為中心的左右兩邊哪些字符已經是對稱的。
我們知道,以pos為中心的左右兩邊對稱,位置 i 在pos的右邊,那么在pos的左邊必然存在和 位置 i 對稱的位置(假設我們記該對稱的位置為 j)。如圖3中的idx=6 。
由于位置 j 已經訪問過,我們知道以位置 j 為中心的回文串回文半徑, 此處分兩種情況討論:
- 以位置 j 為中心的回文串在 位置pos和位置Maxright的對稱位置之間,如圖4所示.
如圖所示,由于以位置 j 為對稱軸的回文串的回文半徑已知,根據對稱性,我們知道位置 i 的左右鄰居對稱,因此可以從左右鄰居開始尋找以位置 i 為對稱抽的回文串,這樣便減少了對字符的重復訪問。
- 以位置 j 為中心的回文串不在 位置pos和Maxright的對稱位置之間,如圖5所示.
此時我們只能確定紅色線條之間字符關于位置 i 對稱,但這也減少了重復訪問字符的次數。此時,只需要從左紅線的左端,右紅線的右端開始遍歷字符、判斷對稱,尋找最長回文字符。
2)當前訪問的位置 i 在 MaxRight的右邊,如圖6。
此時,說明以位置 i 為對稱軸的回文串的左右兩側的對稱信息,無法從歷史信息中推導出來,我們不得不從位置 i 的左右鄰居開始判斷是否相同,指定遇到不同的字符或達到邊界為止。
步驟4:如何更新RL數組, MaxRight變量,位置pos
if(i < MaxRight){
// RL[i] 初始值
RL[i] = min(RL[pos - (i - pos], MaxRight-i)
}else {
// RL[i] 初始值
RL[i] = 1;
}
以位置 i 為對稱軸 從對稱軸的左右RL[i]距離處,同時向左右開始訪問字符,并同時更新RL[i]
MaxRight = RL[i]+i-1 > MaxRight ? RL[i]+i-1 : MaxRight
pos = RL[i]+i-1 > MaxRight ? i : pos;
算法實現
public int findLongestPalindromicSubstring(String s){
// 填充字符, 假設字符#在s中沒有出現過
String cs = "#";
for(int i=0; i<s.length(); ++i){
cs += s.charAt(i);
cs += "#";
}
// 保存最長的回文字符串的長度
int maxlen = 0;
int[] RL = new int[cs.length()];
int maxRight=0, pos=0;
for(int i=0; i<cs.length(); ++i){
// 根據 i 位于maxRight的左邊還是右邊更新RL[i]
if(i < maxRight){
// i 在maxRight左邊的情況
RL[i] = Math.min(RL[2*pos-1], maxRight-i);
}else{
// i 在maxRight右邊的情況
RL[i]=1;
}
// 邊界判斷, 回文判斷
while(i+RL[i]<cs.length() && i-RL[i] >=0 && cs.charAt(i+RL[i])==cs.charAt(i-RL[i])){
++RL[i];
}
if(RL[i]+i-1 > maxRight){
maxRight = RL[i] + i -1;
pos = i;
}
// 更新最長回文字符串的長度
maxlen = maxlen > RL[i] ? RL[i] : maxlen;
}
// 利用RL的性質
return maxlen-1;
}
復雜度分析
空間復雜度:插入分隔符形成新串,占用了線性的空間大小;RL數組也占用線性大小的空間,因此空間復雜度是線性的。
時間復雜度:盡管代碼里面有兩層循環,通過平攤分析,我們可以得出,Manacher的時間復雜度是線性的。由于內層的循環只對尚未匹配的部分進行,因此對于每一個字符而言,只會進行一次,因此時間復雜度是O(n)。
注:文本主要參考了文獻[1],并在理解的基礎上,做了些許更改。