Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.html
給定一個字符串S,找出它的最大的迴文子串,你能夠假設字符串的最大長度是1000,並且存在惟一的最長迴文子串java
動態規劃法,
假設dp[ i ][ j ]的值爲true,表示字符串s中下標從 i 到 j 的字符組成的子串是迴文串。那麼能夠推出:
dp[ i ][ j ] = dp[ i + 1][ j - 1] && s[ i ] == s[ j ]。
這是通常的狀況,因爲須要依靠i+1, j -1,因此有可能 i + 1 = j -1, i +1 = (j - 1) -1,所以須要求出基準狀況才能套用以上的公式:
a. i + 1 = j -1,即迴文長度爲1時,dp[ i ][ i ] = true;
b. i +1 = (j - 1) -1,即迴文長度爲2時,dp[ i ][ i + 1] = (s[ i ] == s[ i + 1])。
有了以上分析就能夠寫出代碼了。須要注意的是動態規劃須要額外的O(n^2)的空間。算法
public class Solution { /** * 005-Longest Palindromic Substring(最長迴文子串) * * @param s 輸入的字符串 * @return 最長迴文子串 */ public String longestPalindrome(String s) { if (s == null || s.length() < 2) { return s; } int maxLength = 0; String longest = null; int length = s.length(); boolean[][] table = new boolean[length][length]; // 單個字符都是迴文 for (int i = 0; i < length; i++) { table[i][i] = true; longest = s.substring(i, i + 1); maxLength = 1; } // 判斷兩個字符是不是迴文 for (int i = 0; i < length - 1; i++) { if (s.charAt(i) == s.charAt(i + 1)) { table[i][i + 1] = true; longest = s.substring(i, i + 2); maxLength = 2; } } // 求長度大於2的子串是不是迴文串 for (int len = 3; len <= length; len++) { for (int i = 0, j; (j = i + len - 1) <= length - 1; i++) { if (s.charAt(i) == s.charAt(j)) { table[i][j] = table[i + 1][j - 1]; if (table[i][j] && maxLength < len) { longest = s.substring(i, j + 1); maxLength = len; } } else { table[i][j] = false; } } } return longest; } }
最長迴文子串把原字符串S倒轉過來成爲S‘,覺得這樣就將問題轉化成爲了求S和S’的最長公共子串的問題,而這個問題是典型的DP問題,可是很是惋惜,這個算法是不完善的。數組
S=「c a b a」 那麼 S' = 「a b a c」, 這樣的狀況下 S和 S‘的最長公共子串是aba。沒有錯誤。app
可是當 S=「abacdfgdcaba」, 那麼S’ = 「abacdgfdcaba」。 這樣S和S‘的最長公共子串是abacd。很明顯abacd並非S的最長迴文子串,它甚至連回文都不是。優化
因此最長迴文子串不能轉化成爲最長公共子串問題了。當原串S中含有一個非迴文的串的反序串的時候,最長公共子串的解法就是不正確的。正如上一個例子中S既含有abacd,又含有abacd的反串cdaba,而且abacd又不是迴文,因此轉化成爲最長公共子串的方法不能成功。除非每次咱們求出一個最長公共子串的時候,咱們檢查一下這個子串是否是一個迴文,若是是,那這個子串就是原串S的最長迴文子串;若是不是,那麼就去求下一個次長公共子串,以此類推。ui
最長迴文子串有不少方法,分別是1暴力法,2 動態規劃, 3 從中心擴展法,4 著名的manacher算法。url
方法一 暴力法spa
遍歷字符串S的每個子串,去判斷這個子串是否是迴文,是迴文的話看看長度是否是比最大的長度maxlength大。遍歷每個子串的方法要O(N2),判斷每個子串是否是迴文的時間複雜度是O(N),因此暴利方法的總時間複雜度是O(N3)。.net
方法二 動態規劃 時間複雜度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)
這個算法作了一個簡單的處理,很巧妙地把奇數長度迴文串與偶數長度迴文串統一考慮,也就是在每一個相鄰的字符之間插入一個分隔符,串的首尾也要加,固然這個分隔符不能再原串中出現,通常能夠用‘#’或者‘$’等字符。例如:
原串:abaab
新串:#a#b#a#a#b#
這樣一來,原來的奇數長度迴文串仍是奇數長度,偶數長度的也變成以‘#’爲中心奇數迴文串了。
接下來就是算法的中心思想,用一個輔助數組P 記錄以每一個字符爲中心的最長迴文半徑,也就是P[i]記錄以Str[i]字符爲中心的最長迴文串半徑。P[i]最小爲1,此時迴文串爲Str[i]自己。
咱們能夠對上述例子寫出其P 數組,以下
新串: # a # b # a # a # b #
P[] : 1 2 1 4 1 2 5 2 1 2 1
咱們能夠證實P[i]-1 就是以Str[i]爲中心的迴文串在原串當中的長度。
證實:
一、顯然L=2*P[i]-1 即爲新串中以Str[i]爲中心最長迴文串長度。
二、以Str[i]爲中心的迴文串必定是以#開頭和結尾的,例如「#b#b#」或「#b#a#b#」因此L 減去最前或者最後的‘#’字符就是原串中長度 的二倍,即原串長度爲(L-1)/2,化簡的P[i]-1。得證。 依次從前日後求得P 數組就能夠了,這裏用到了DP(動態規劃)的思想, 也就是求P[i] 的時候,前面的P[]值已經獲得了,咱們利用迴文串的特殊性質能夠進行一個大大的優化。
先把核心代碼貼上:
[cpp] view plain copy
爲了防止求P[i]向兩邊擴展時可能數組越界,咱們須要在數組最前面和最後面加一個特殊字符,令P[0]=‘$’最後位置默認爲‘\0’不須要特殊處理。此外,咱們用MaxId 變量記錄在求i 以前的迴文串中,延伸至最右端的位置,同時用id 記錄取這個MaxId 的id 值。經過下面這句話,算法避免了不少不必的重複匹配。
[cpp] view plain copy
那麼這句話是怎麼得來的呢,其實就是利用了迴文串的對稱性,以下圖,
j=2*id-1 即爲i 關於id 的對稱點,根據對稱性,P[j]的迴文串也是能夠對稱到i 這邊的,可是若是P[j]的迴文串對稱過來之後超過MaxId 的話,超出部分就不能對稱過來了,以下圖,
因此這裏P[i]爲的下限爲二者中的較小者,p[i]=Min(p[2*id-i],MaxId-i)。算法的有效比較次數爲MaxId 次,因此說這個算法的時間複雜度爲O(n)。
題目:給一個字符串,找出最長的迴文的長度(或求這個迴文)。
分析:
尋找字符串中的迴文,有特定的算法來解決,也是本文的主題:Manacher算法,其時間複雜度爲O(n)。
首先在每兩個相鄰字符中間插入一個分隔符,固然這個分隔符要在原串中沒有出現過。通常能夠用‘#’分隔。這樣就很是巧妙的將奇數長度迴文串與偶數長度迴文串統一塊兒來考慮了。
而後,咱們須要一個輔助數組rad[],用rad[i]表示第i個字符的迴文半徑,rad[i]的最小值爲1,即只有一個字符的狀況,如今問題轉變成如何求出rad數組。
假設如今求出了rad[1, ..., i],如今要求後面的rad值,再假設如今有個指針k,從1循環到rad[i],試圖經過某些手段來求出[i + 1, i + rad[i] - 1]的rad值,其分析以下:
如圖1所示,黑色的部分是一個迴文子串,兩段紅色的區間對稱相等。由於以前已經求出了rad[i - k],因此能夠避免一些重複的查找和判斷,有3種狀況:
圖1
① rad[i] - k < rad[i - k]
如圖1,rad[i - k]的範圍爲青色。由於黑色的部分是迴文的,且青色的部分超過了黑色的部分,因此rad[i + k]確定至少爲rad[i]-k,即橙色的部分。那橙色之外的部分就不是了嗎?這是確定的,由於若是橙色之外的部分也是迴文的,那麼根據青色和紅色部分的關係,能夠證實黑色部分再往外延伸一點也是一個迴文子串,這確定是不可能的,所以rad[i + k] = rad[i] - k。
② rad[i] - k > rad[i - k]
如圖2,rad[i-k]的範圍爲青色,由於黑色的部分是迴文的,且青色的部分在黑色的部分裏面,根據定義,很容易得出:rad[i + k] = rad[i - k]。根據上面兩種狀況,能夠得出結論:當rad[i] - k != rad[i - k]的時候,rad[i + k] = min(rad[i] - k, rad[i - k])。
圖2
③ rad[i] - k = rad[i - k]
如圖,經過和第一種狀況對比以後會發現,由於青色的部分沒有超出黑色的部分,因此即便橙色的部分全等,也沒法像第一種狀況同樣引出矛盾,所以橙色的部分是有可能全等的。可是,根據已知的信息,咱們不知道橙色的部分是多長,所以就須要再去嘗試和判斷了。
圖3
以上就是Manacher算法的核心思想。POJ上有一道關於迴文的題目POJ3974,讀者瞭解Manacher算法以後有興趣能夠作作,下面給出該題的Java代碼,能夠經過。
import java.io.FileNotFoundException; import java.util.Scanner;
public class Main {
public static int getPalindromeLength(String str) { // 1.構造新的字符串 // 爲了不奇數迴文和偶數迴文的不一樣處理問題,在原字符串中插入'#',將全部迴文變成奇數迴文 StringBuilder newStr = new StringBuilder(); newStr.append('#'); for (int i = 0; i < str.length(); i ++) { newStr.append(str.charAt(i)); newStr.append('#'); }
// rad[i]表示以i爲中心的迴文的最大半徑,i至少爲1,即該字符自己 int [] rad = new int[newStr.length()]; // right表示已知的迴文中,最右的邊界的座標 int right = -1; // id表示已知的迴文中,擁有最右邊界的迴文的中點座標 int id = -1; // 2.計算全部的rad // 這個算法是O(n)的,由於right只會隨着裏層while的迭代而增加,不會減小。 for (int i = 0; i < newStr.length(); i ++) { // 2.1.肯定一個最小的半徑 int r = 1; if (i <= right) { r = Math.min(rad[id] - i + id, rad[2 * id - i]); } // 2.2.嘗試更大的半徑 while (i - r >= 0 && i + r < newStr.length() && newStr.charAt(i - r) == newStr.charAt(i + r)) { r++; } // 2.3.更新邊界和迴文中心座標 if (i + r - 1> right) { right = i + r - 1; id = i; } rad[i] = r; }
// 3.掃描一遍rad數組,找出最大的半徑 int maxLength = 0; for (int r : rad) { if (r > maxLength) { maxLength = r; } } return maxLength - 1; }
public static void main(String[] args) throws FileNotFoundException { int caseNum = 0; Scanner sc = new Scanner(System.in); while (true) { String str = sc.nextLine(); if (str.equals("END")) { break; } else { caseNum ++; System.out.println("Case " + caseNum + ": " + getPalindromeLength(str)); } } }
} |