最長迴文子串問題:給定一個字符串,求它的最長迴文子串長度。java
若是一個字符串正着讀和反着讀是同樣的,那它就是迴文串。下面是一些迴文串的實例:python
12321 a aba abba aaaa tattarrattat(牛津英語詞典中最長的迴文單詞)
對於最長迴文子串問題,最簡單粗暴的辦法是:找到字符串的全部子串,遍歷每個子串以驗證它們是否爲迴文串。一個子串由子串的起點和終點肯定,所以對於一個長度爲n的字符串,共有n^2個子串。這些子串的平均長度大約是n/2,所以這個解法的時間複雜度是O(n^3)。算法
顯然全部的迴文串都是對稱的。長度爲奇數迴文串以最中間字符的位置爲對稱軸左右對稱,而長度爲偶數的迴文串的對稱軸在中間兩個字符之間的空隙。能否利用這種對稱性來提升算法效率呢?答案是確定的。咱們知道整個字符串中的全部字符,以及字符間的空隙,均可能是某個迴文子串的對稱軸位置。能夠遍歷這些位置,在每一個位置上同時向左和向右擴展,直到左右兩邊的字符不一樣,或者達到邊界。對於一個長度爲n的字符串,這樣的位置一共有n+n-1=2n-1個,在每一個位置上平均大約要進行n/4次字符比較,因而此算法的時間複雜度是O(n^2)。數組
對於一個比較長的字符串,O(n^2)的時間複雜度是難以接受的。Can we do better? spa
先來看看解法2存在的缺陷。翻譯
1) 因爲迴文串長度的奇偶性形成了不一樣性質的對稱軸位置,解法2要對兩種狀況分別處理;
2) 不少子串被重複屢次訪問,形成較差的時間效率。code
缺陷2)能夠經過這個直觀的小?體現:rem
char: a b a b a i : 0 1 2 3 4
當i==1,和i==2時,左邊的子串aba分別被遍歷了一次。字符串
若是咱們能改善解法2的不足,就頗有但願能提升算法的效率。Manacher正是針對這些問題改進算法。it
Manacher算法首先對字符串作一個預處理,在全部的空隙位置(包括首尾)插入一樣的符號,要求這個符號是不會在原串中出現的。這樣會使得全部的串都是奇數長度的。以插入#號爲例:
aba ———> #a#b#a# abba ———> #a#b#b#a#
插入的是一樣的符號,且符號不存在於原串,所以子串的迴文性不受影響,原來是迴文的串,插完以後仍是迴文的,原來不是迴文的,依然不會是迴文。
咱們把一個迴文串中最左或最右位置的字符與其對稱軸的距離稱爲迴文半徑。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
的左邊仍是右邊。咱們分狀況來討論。
i
在MaxRight
的左邊狀況1)能夠用下圖來刻畫:
咱們知道,圖中兩個紅色塊之間(包括紅色塊)的串是迴文的;而且以i
爲對稱軸的迴文串,是與紅色塊間的迴文串有所重疊的。咱們找到i
關於pos
的對稱位置j
,這個j
對應的RL[j]
咱們是已經算過的。根據迴文串的對稱性,以i
爲對稱軸的迴文串和以j
爲對稱軸的迴文串,有一部分是相同的。這裏又有兩種細分的狀況。
以j
爲對稱軸的迴文串比較短,短到像下圖這樣。
這時咱們知道RL[i]至少不會小於RL[j],而且已經知道了部分的以i
爲中心的迴文串,因而能夠令RL[i]=RL[j]
。可是以i
爲對稱軸的迴文串可能實際上更長,所以咱們試着以i
爲對稱軸,繼續往左右兩邊擴展,直到左右兩邊字符不一樣,或者到達邊界。
以j
爲對稱軸的迴文串很長,這麼長:
這時,咱們只能肯定,兩條藍線之間的部分(即不超過MaxRight的部分)是迴文的,因而從這個長度開始,嘗試以i
爲中心向左右兩邊擴展,,直到左右兩邊字符不一樣,或者到達邊界。
不論以上哪一種狀況,以後都要嘗試更新MaxRight
和pos
,由於有可能獲得更大的MaxRight。
具體操做以下:
step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i) step 2: 以i爲中心擴展迴文串,直到左右兩邊字符不一樣,或者到達邊界。 step 3: 更新MaxRight和pos
i
在MaxRight
的右邊
遇到這種狀況,說明以i
爲對稱軸的迴文串尚未任何一個部分被訪問過,因而只能從i
的左右兩邊開始嘗試擴展了,當左右兩邊字符不一樣,或者到達字符串邊界時中止。而後更新MaxRight
和pos
。
#Python 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
空間複雜度:插入分隔符造成新串,佔用了線性的空間大小;RL數組也佔用線性大小的空間,所以空間複雜度是線性的。
時間複雜度:儘管代碼裏面有兩層循環,經過amortized analysis咱們能夠得出,Manacher的時間複雜度是線性的。因爲內層的循環只對還沒有匹配的部分進行,所以對於每個字符而言,只會進行一次,所以時間複雜度是O(n)。
4.1 人們在一座名爲赫庫蘭尼姆的古城遺蹟中,找到了一個好玩的拉丁語迴文串:sator arepo tenet opera rotas
。翻譯成中文大概就是`一個叫作Arepo的播種者,他用力地扶(把)着車輪。這個串的每一個單詞首字母恰好組成了第一個單詞,每一個單詞的第二個字母恰好組成了第二個單詞...因而乎,若是寫出醬紫,你會發現上下左右四個方向讀起來是同樣的。這個串被稱爲 Sator Square.
4.2 本文開頭給出的單詞tattarrattat
,出如今愛爾蘭做家詹姆斯·喬伊斯的小說《尤利西斯》,是敲門的意思。吉尼斯紀錄的最長迴文英文單詞是detartrated
,是個化學術語。另外,還有些已出版的英文迴文小說(大家歪果仁真會玩),好比Satire: Veritas,Dr Awkward & Olson in Oslo等。
能夠採用動態規劃,列舉迴文串的起點或者終點來解最長迴文串問題,無需討論串長度的奇偶性。
看下面的扎瓦代碼,容易理解。
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; }