最長回文子串的不同解法
給定一個字符串,返回該字符串的最長回文子串。回文也就是說 。正著讀和反著讀是一樣的。以下總結了幾種求回文的方式:
方法1 : 非常easy,枚舉全部的區間 [i,j] ,查看該範圍內是否是一個回文.
時間復雜度 O(n^3),空間復雜度 O(1).
方法2: 方法1的時間復雜度太高,而且存在著大量的反復運算。能夠使用DP來解。而且保存已經檢查過的字符串的狀態.
時間復雜度: O(n^2)。空間復雜度O(n^2).
這裏存在兩種DP的方法,是依據區間來進行DP,還是長度。只是都是大同小異,不改變整個算法的時間復雜度。
代碼例如以下:
//dp 1 string LongestPalindrome(const string &s) { const int n = s.size(); if(n < 2) return s; bool f[n][n+1]; fill_n(&f[0][0],n*(n+1),false); int start = 0, len = 1; f[0][0] = true; for(int i=0;i<n;++i) { f[i][0] = true; f[i][1] = true; } for(int i=n-2;i>=0;--i) { for(int j=2;j<=n && (i+j-1)<n;++j) { f[i][j] = f[i+1][j-2] && s[i] == s[i+j-1]; if(f[i][j] && j > len) {start = i; len = j;} } } return s.substr(start,len); } //dp 2 string LongestPalindrome_dp2(const string &s) { const int n = s.size(); if(n < 2) return s; bool f[n][n]; fill_n(&f[0][0],n*n,false); int start=0,len=1; f[0][0] = true; for(int i=0;i<n;++i) f[i][i] = true; for(int i = n-1 ; i >= 0; --i) { for(int j = i+1; j < n;++j) { if(j == i+1) f[i][j] = (s[i] == s[j]); else f[i][j] = f[i+1][j-1] && s[i] == s[j]; if(f[i][j] && (j-i+1) > len) {start = i; len = j-i+1;} } } return s.substr(start,len); }
方法3: 非常直觀的想法。以每個字符串為中心,計算該字符串左右能夠延伸的部分。註意處理長度為奇數和偶數的情況。
時間復雜度 : O(n^2) 。空間復雜度 : O(1)
//從中間往兩端延伸(考慮奇數偶數的情況就可以) string LongestPalindrome_extend(const string &s) { const int n = s.size(); if(n < 2) return s; int low,high; int start=0,len=1; for(int i=1;i<n;++i) { //even low = i-1; high = i; while(low>=0&&high<n&&s[low]==s[high]) { if(high-low+1>len) { start=low; len=high-low+1; } --low;++high; } //odd low = i-1; high = i+1; while(low>=0&&high<n&&s[low]==s[high]) { if(high-low+1 > len) { start = low; len=high-low+1; } --low;++high; } } return s.substr(start,len); }
方法4:使用後綴數組的思想,將字符串s取s的逆,拼接在s的後面,也就是說 如今考察的字符串是 s#s‘。當中的#是額外的一個字符,s‘是s的逆串。求當前這個新拼接而成的字符串的後綴樹組的最長公共前綴。
時間復雜度: O(n^2),空間復雜度 O(n^2)
//關於此方法還沒想明確。暫不貼代碼
方法5: manacher算法。此算法也就是直接參考的上述的方法3,以每個點為中心,來計算左右能夠延伸的部分,可是這個方案存在冗余的比較,manacher則是利用已經有的信息,盡可能的降低冗余的信息。
詳細請參考 點擊打開鏈接。
以下的圖是我對manacher算法的理解,manacher算法事實上就是計算一個數組。數組中的每個元素表示以當前元素為中心的回文的長度。事實上是非常easy的,僅僅要分情況來討論就能夠了。
對上述的理解,當前須要計算的位置的index為 i,此時的最右端的位置是right,這個right相應的回文的中心為idx。
分兩種情況來討論:
1 right <= i , 也就是最以下的一幅圖,非常顯然之前計算過的回文的信息對於計算此時的 i 的回文是全然沒有幫助的,也就是說。此時 須要以 i 為中心一個一個的去匹配就可以。
2 right > i , 也就是中間3三幅圖的情況,圖中的 j 表示以 idx 為對稱中心的 i 的對稱點的位置, 顯然 j = 2 * id - i 。
這裏又分兩種情況:
1) 假設以 j 為中心的回文子串的左邊界超出了以idx為中心的回文(圖4)。那麽這時的 i 的回文子串的長度除了至少能夠到達right。 至於超出right的部分,僅僅好一個一個的去匹配了。
2) 假設以j為中心的回文子串的左邊界沒有超出以idx為中心的回文。那麽直接就是j的回文的長度就可以。
也就是說, 假設 i > right ,那麽P[right] = 1;
假設 i < right。此時的 P[i] 的值取決於 i 關於 idx 的對稱點 j 的 P[j]的值 。 假設 i + P[j] > right ,那麽P[i] = right-i ,余下的部分一個一個的去匹配。 假設 i + P[j] < right。那麽P[i] = P[j] ,剩下的一個一個去匹配。
時間復雜度 : O(n), 空間復雜度 O(n).
代碼為:
//Manacher O(n) string Manacher(const string &str) { //add ‘#‘ string s = "$"; for(auto a : str) { s += ‘#‘; s += a; } s += ‘#‘; cout << s << endl; const int n = s.size(); vector<int> P(n,0); int right = -1, idx = -1; //right記錄當前已經計算過的回文的最右邊的邊界(這個邊界是不包括在回文中的) for(int i=1;i<n;++i) { P[i] = (right > i)? min(P[2*idx-i],right-i):1; //這一句就是整個算法的核心!!
!!
while(s[i+P[i]] == s[i-P[i]])P[i]++; if(i+P[i]>right) { right = i + P[i]; idx = i; } } auto pos = max_element(P.begin(),P.end()); int len = *pos-1; string ret; int i = pos-P.begin(); //print ret += s[i]; cout << ret << endl; int k=1; while(len) { ret += s[i+k]; ret = s[i-k]+ret; cout << ret << endl; ++k; --len; } //trim # string ret2; for(auto a :ret) if(a!=‘#‘)ret2 += a; return ret2; }
上述代碼均已驗證正確。至於原理,全在代碼中。
最長回文子串的不同解法