最長迴文子串

最長迴文子串是最初我在網易筆試的時候遇見的,當時天真的把原字符串S倒轉過來成爲S‘,以爲這樣就將問題轉化成爲了求S和S’的
最長公共子串
的問題,而這個問題是典型的DP問題,我也在前面的文章中介紹了3中解決這個問題的方法。但是非常可惜,後來才知道這個算法是不完善的。那麼到底爲什麼呢?聽我慢慢道來。

S=“c a b a”  那麼  S’ = “a b a c”, 這樣的情況下 S和 S‘的最長公共子串是aba。沒有錯誤。

    但是當 S=“abacdfgdcaba”, 那麼S’ = “abacdgfdcaba”。 這樣S和S‘的最長公共子串是abacd。很明顯abacd並不是S的最長迴文子串,它甚至連回文都不是。

    現在是不是都明白爲什麼最長迴文子串不能轉化成爲最長公共子串問題了。當原串S中含有一個非迴文的串的反序串的時候,最長公共子串的解法就是不正確的。正如上一個例子中S既含有abacd,又含有abacd的反串cdaba,並且abacd又不是迴文,所以轉化成爲最長公共子串的方法不能成功。除非每次我們求出一個最長公共子串的時候,我們檢查一下這個子串是不是一個迴文,如果是,那這個子串就是原串S的最長迴文子串;如果不是,那麼就去求下一個次長公共子串,以此類推。

最長迴文子串有很多方法,分別是1暴力法,2 動態規劃, 3 從中心擴展法,4 著名的manacher算法。下面我將分別介紹幾種方法。

方法一 暴力法

遍歷字符串S的每一個子串,去判斷這個子串是不是迴文,是迴文的話看看長度是不是比最大的長度maxlength大。遍歷每一個子串的方法要O(N2),判斷每一個子串是不是迴文的時間複雜度是O(N),所以暴利方法的總時間複雜度是O(N3)。


方法二 動態規劃 時間複雜度O(N2), 空間複雜度O(N2)

    動態規劃就是暴力法的進化版本,我們沒有必要對每一個子串都重新計算,看看它是不是迴文。我們可以記錄一些我們需要的東西,就可以在O(1)的時間判斷出該子串是不是一個迴文。這樣就比暴力法節省了O(N)的時間複雜度哦,嘿嘿,其實優化很簡單吧。

P(i,j)爲1時代表字符串Si到Sj是一個迴文,爲0時代表字符串Si到Sj不是一個迴文。

P(i,j)= P(i+1,j-1)(如果S[i] = S[j])。這是動態規劃的狀態轉移方程。

P(i,i)= 1,P(i,i+1)= if(S[i]= S[i+1])

string longestPalindromeDP(string s) {

  int n = s.length();

  int longestBegin = 0;

  int maxLen = 1;

  bool table[1000][1000] = {false};

  for (int i = 0; i < n; i++) {

    table[i][i] = true; //前期的初始化

  }

  for (int i = 0; i < n-1; i++) {

    if (s[i] == s[i+1]) {

      table[i][i+1] = true; //前期的初始化

      longestBegin = i;

      maxLen = 2;

    }

  }

  for (int len = 3; len <= n; len++) {

    for (int i = 0; i < n-len+1; i++) {

      int j = i+len-1;

      if (s[i] == s[j] && table[i+1][j-1]) {

        table[i][j] = true;

        longestBegin = i;

        maxLen = len;

      }

    }

  }

  return s.substr(longestBegin, maxLen);

}

方法三 中心擴展法

    這個算法思想其實很簡單啊,時間複雜度爲O(N2),空間複雜度僅爲O(1)。就是對給定的字符串S,分別以該字符串S中的每一個字符C爲中心,向兩邊擴展,記錄下以字符C爲中心的迴文子串的長度。但是有一點需要注意的是,迴文的情況可能是 a b a,也可能是 a b b a。

string expandAroundCenter(string s, int c1, int c2) {

  int l = c1, r = c2;

  int n = s.length();

  while (l >= 0 && r <= n-1 && s[l] == s[r]) {

    l--;

    r++;

  }

  return s.substr(l+1, r-l-1);

}

string longestPalindromeSimple(string s) {

  int n = s.length();

  if (n == 0) return "";

  string longest = s.substr(0, 1);  // a single char itself is a palindrome

  for (int i = 0; i < n-1; i++) {

    string p1 = expandAroundCenter(s, i, i);

    if (p1.length() > longest.length())

      longest = p1;

    string p2 = expandAroundCenter(s, i, i+1);

    if (p2.length() > longest.length())

      longest = p2;

  }

  return longest;

}

  方法四 傳說中的Manacher算法。時間複雜度O(N)

    這個算法有一個很巧妙的地方,它把奇數的迴文串和偶數的迴文串統一起來考慮了。這一點一直是在做迴文串問題中時比較煩的地方。這個算法還有一個很好的地方就是充分利用了字符匹配的特殊性,避免了大量不必要的重複匹配。

    算法大致過程是這樣。先在每兩個相鄰字符中間插入一個分隔符,當然這個分隔符要在原串中沒有出現過。一般可以用‘#’分隔。這樣就非常巧妙的將奇數長度迴文串與偶數長度迴文串統一起來考慮了(見下面的一個例子,迴文串長度全爲奇數了),然後用一個輔助數組P記錄以每個字符爲中心的最長迴文串的信息。P[id]記錄的是以字符str[id]爲中心的最長迴文串,當以str[id]爲第一個字符,這個最長迴文串向右延伸了P[id]個字符。

    原串:    w aa bwsw f d

    新串:           #  w  # a # a #  b  # w # s # w #  f  # d #
輔助數組P: 1  2  1 2 3 2 1  2  1  2 1 4 1 2 1  2 1 2 1

這裏有一個很好的性質,P[id]-1就是該回文子串在原串中的長度(包括‘#’)。如果這裏不是特別清楚,可以自己拿出紙來畫一畫,自己體會體會。當然這裏可能每個人寫法不盡相同,不過我想大致思路應該是一樣的吧。

現在的關鍵問題就在於怎麼在O(n)時間複雜度內求出P數組了。只要把這個P數組求出來,最長迴文子串就可以直接掃一遍得出來了。

    那麼怎麼計算P[i]呢?該算法增加兩個輔助變量(其實一個就夠了,兩個更清晰)id和mx,其中id表示最大回文子串中心的位置,mx則爲id+P[id],也就是最大回文子串的邊界。

    然後可以得到一個非常神奇的結論,這個算法的關鍵點就在這裏了:如果mx > i,那麼

P[i] >= MIN(P[2 * id – i], mx – i)。就是這個串卡了我非常久。實際上如果把它寫得複雜一點,理解起來會簡單很多:

//記j = 2 * id – i,也就是說 j 是 i 關於 id 的對稱點。

if (mx – i > P[j]) 

    P[i] = P[j];

else /* P[j] >= mx – i */

    P[i] = mx – i; // P[i] >= mx – i,取最小值,之後再匹配更新。

當 mx – i > P[j] 的時候,以S[j]爲中心的迴文子串包含在以S[id]爲中心的迴文子串中,由於 i 和 j 對稱,以S[i]爲中心的迴文子串必然包含在以S[id]爲中心的迴文子串中,所以必有 P[i] = P[j]。


當 P[j] > mx – i 的時候,以S[j]爲中心的迴文子串不完全包含於以S[id]爲中心的迴文子串中,但是基於對稱性可知,也就是說以S[i]爲中心的迴文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx – i。至於mx之後的部分是否對稱,就只能老老實實去匹配了。

由於這個算法是線性從前往後掃的。那麼當我們準備求P[i]的時候,i以前的P[j]我們是已經得到了的。我們用mx記在i之前的迴文串中,延伸至最右端的位置。同時用id這個變量記下取得這個最優mx時的id值。(注:爲了防止字符比較的時候越界,我在這個加了‘#’的字符串之前還加了另一個特殊字符‘$’,故我的新串下標是從1開始的)

#include<vector> #include<iostream> using namespace std; const int N=300010; int n, p[N]; char s[N], str[N]; #define _min(x, y) ((x)<(y)?(x):(y)) void kp() { int i; int mx = 0; int id; for(i=n; str[i]!=0; i++) str[i] = 0; //沒有這一句有問題。。就過不了ural1297,比如數據:ababa aba for(i=1; i<n; i++) { if( mx > i ) p[i] = _min( p[2*id-i], p[id]+id-i ); else p[i] = 1; for(; str[i+p[i]] == str[i-p[i]]; p[i]++) ; if( p[i] + i > mx ) { mx = p[i] + i; id = i; } } } void init() { int i, j, k; str[0] = '$'; str[1] = '#'; for(i=0; i<n; i++) { str[i*2+2] = s[i]; str[i*2+3] = '#'; } n = n*2+2; s[n] = 0; } int main() { int i, ans; while(scanf("%s", s)!=EOF) { n = strlen(s); init(); kp(); ans = 0; for(i=0; i<n; i++) if(p[i]>ans) ans = p[i]; printf("%d\n", ans-1); } return 0; }


if( mx > i)

p[i]=MIN( p[2*id-i], mx-i);

就是當前面比較的最遠長度mx>i的時候,P[i]有一個最小值。這個算法的核心思想就在這裏,爲什麼P數組滿足這樣一個性質呢?

(下面的部分爲圖片形式)

《最長迴文子串》

LEETCODE上也有這個題的詳細說明,不過是英文版本的。
http://www.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html

点赞