題目:
Given a string S, find the longest palindromic substring in S.
給出一個字符串S,找到一個最長的連續迴文串。
例如串 babcbabcbaccba 最長迴文是:abcbabcba
這個題目小弟給出3中解法,前兩種的都是 O(n^2), 第三種思路是O(n).
思路1. 動態規劃
這裏動態規劃的思路是 dp[i][j] 表示的是 從i 到 j 的字串,是否是迴文串。
則根據迴文的規則我們可以知道:
如果s[i] == s[j] 那麼是否是迴文決定於 dp[i+1][ j - 1]
當 s[i] != s[j] 的時候, dp[i][j] 直接就是 false。
動態規劃的進行是按照字符串的長度從1 到 n推進的。
代碼很明晰:給出java代碼,複雜度 O(n^2)
- public class DPSolution {
- boolean[][] dp;
- public String longestPalindrome(String s)
- {
- if(s.length() == 0)
- {
- return "";
- }
- if(s.length() == 1)
- {
- return s;
- }
- dp = new boolean[s.length()][s.length()];
- int i,j;
- for( i = 0; i < s.length(); i++)
- {
- for( j = 0; j < s.length(); j++)
- {
- if(i >= j)
- {
- dp[i][j] = true; //當i == j 的時候,只有一個字符的字符串; 當 i > j 認爲是空串,也是迴文
- }
- else
- {
- dp[i][j] = false; //其他情況都初始化成不是迴文
- }
- }
- }
- int k;
- int maxLen = 1;
- int rf = 0, rt = 0;
- for( k = 1; k < s.length(); k++)
- {
- for( i = 0; k + i < s.length(); i++)
- {
- j = i + k;
- if(s.charAt(i) != s.charAt(j)) //對字符串 s[i....j] 如果 s[i] != s[j] 那麼不是迴文
- {
- dp[i][j] = false;
- }
- else //如果s[i] == s[j] 迴文性質由 s[i+1][j-1] 決定
- {
- dp[i][j] = dp[i+1][j-1];
- if(dp[i][j])
- {
- if(k + 1 > maxLen)
- {
- maxLen = k + 1;
- rf = i;
- rt = j;
- }
- }
- }
- }
- }
- return s.substring(rf, rt+1);
- }
- }
思路2. KMP匹配
第二個思路來源於字符串匹配,最長迴文串有如下性質:
對於串S, 假設它的 Reverse是 S', 那麼S的最長迴文串是 S 和 S' 的最長公共字串。
例如 S = abcddca, S' = acddcba, S和S'的最長公共字串是 cddc 也是S的最長迴文字串。
如果S‘是 模式串,我們可以對S’的所有後綴枚舉(S0, S1, S2, Sn) 然後用每個後綴和S匹配,尋找最長的匹配前綴。
例如當前枚舉是 S0 = acddcba 最長匹配前綴是 a
S1 = cddcba 最長匹配前綴是 cddc
S2 = ddcba 最長匹配前綴是 ddc
當然這個過程可以做適當剪枝,如果當前枚舉的後綴長度,小於當前找到的最長匹配,則直接跳過。
Java 代碼如下:
- public class Solution {
- private int[] next;
- private void GetNext(String s) //KMP求next數組
- {
- int i,j;
- i = 0;
- j = -1;
- next[0] = -1;
- while( i < s.length())
- {
- if( j == -1 || s.charAt(i) == s.charAt(j))
- {
- i++;
- j++;
- next[i] = j;
- }
- else
- {
- j = next[j];
- }
- }
- }
- private int compare(String pattern, String s) //用KMP算法做求出最長的前綴匹配
- {
- int i,j;
- i = 0;
- j = 0;
- int maxLen = 0;
- while( i < s.length())
- {
- if(j == -1 || pattern.charAt(j) == s.charAt(i))
- {
- i++;
- j++;
- }
- else
- {
- j = next[j];
- }
- if( j > maxLen)
- {
- maxLen = j;
- }
- if(j == pattern.length())
- {
- return maxLen;
- }
- }
- return maxLen;
- }
- public String longestPalindrome(String s) //
- {
- // Start typing your Java solution below
- // DO NOT write main() function
- String reverString = new StringBuilder(s).reverse().toString(); //求得到 輸入string 的reverse
- next = new int[s.length() + 1];
- String maxPal = "";
- int maxLen = 0;
- int len;
- for(int i = 0; i < s.length(); i++) //枚舉所有後綴
- {
- String suffix = reverString.substring(i);
- if(suffix.length() < maxLen)
- {
- break;
- }
- GetNext(suffix);
- len = compare(suffix, s);
- if( len > maxLen)
- {
- maxPal = suffix.substring(0, len);
- maxLen = len;
- }
- }
- return maxPal;
- }
- }
思路3. 思路來源於此
http://www.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html
不過原文的陳述仔細研究了一下,有一些地方讓人着實費解,所以自己決定重寫一遍。
這裏描述了一個叫Manacher’s Algorithm的算法。
算法首先將輸入字符串S, 轉換成一個特殊字符串T,轉換的原則就是將S的開頭結尾以及每兩個相鄰的字符之間加入一個特殊的字符,例如#
例如: S = “abaaba”, T = “#a#b#a#a#b#a#”.
爲了找到最長的迴文字串,例如我們當前考慮以Ti爲迴文串中間的元素,如果要找到最長迴文字串,我們要從當前的Ti擴展使得 Ti-d … Ti+d 組成最長迴文字串. 這裏 d 其實和 以Ti爲中心的迴文串長度是一樣的. 進一步解釋就是說,因爲我們這裏插入了 # 符號,對於一個長度爲偶數的迴文串,他應該是以#做爲中心的,然後向兩邊擴,對於長度是奇數的迴文串,它應該是以一個普通字符作爲中心的。通過使用#,我們將無論是奇數還是偶數的迴文串,都變成了一個以Ti爲中心,d爲半徑兩個方向擴展的問題。並且d就是迴文串的長度。
例如 #a#b#a#, P = 0103010, 對於b而言P的值是3,是最左邊的#,也是延伸的最左邊。這個值和當前的迴文串是一致的。
如果我們求出所有的P值,那麼顯然我們要的迴文串,就是以最大P值爲中心的迴文串。
T = # a # b # a # a # b # a # P = 0 1 0 3 0 1 6 1 0 3 0 1 0
例如上面的例子,最長迴文是 “abaaba”, P6 = 6.
根據觀察發現,如果我們在一個位置例如 abaaba的中間位置,用一個豎線分開,兩側的P值是對稱的。當然這個性質不是在任何時候都會成立,接下來就是分析如何利用這個性質,使得我們可以少算很多P的值。
下面的例子 S = “babcbabcbaccba” 存在更多的摺疊迴文字串。
當時當i = 15的時候,卻只能得到迴文 “a#b#c#b#a”, 長度是5, 而對稱 i ' = 7 的長度是7.
如上圖所示,如果以 i, i' 爲中心,畫出對稱的區域如圖,其中以i‘ = 7 對稱的區域是 實心綠色 + 虛綠色 和 左側,虛綠色表示當前的對稱長度已經超過之前的對稱中心C。而之前的P對稱性質成立的原因是 i 右側剩餘的長度 R - i 正好比 以 i‘ 爲中心的迴文小。
then P[ i ] ← P[ i' ]
else P[ i ] ≥R – i. (這裏下一步操作是擴充 P[ i ].
擴充P[i] 之後,我們還要做一件事情是更新 R 和 C, 如果當前對稱中心的最右延伸大於R,我們就更新C和R。在迭代的過程中,我們試探i的時候,如果P[i'] <= R - i, 那麼只要做一件事情。 如果不成立我們對當前P[i] 做擴展,因爲最大長度是n,擴展最多就做n次,所以最多做2*n。 所以最後算法複雜度是 O(n)
或許貼上代碼更容易一些。直接使用大神的代碼了,雖然自己也實現了,不過是理解大神的思路實現的。
// Transform S into T.
// For example, S = "abba", T = "^#a#b#b#a#$".
// ^ and $ signs are sentinels appended to each end to avoid bounds checking
string preProcess(string s) {
int n = s.length();
if (n == 0) return "^$";
string ret = "^";
for (int i = 0; i < n; i++)
ret += "#" + s.substr(i, 1);
ret += "#$";
return ret;
}
string longestPalindrome(string s) {
string T = preProcess(s);
int n = T.length();
int *P = new int[n];
int C = 0, R = 0;
for (int i = 1; i < n-1; i++) {
int i_mirror = 2*C-i; // equals to i' = C - (i-C)
P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;
// Attempt to expand palindrome centered at i
while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
P[i]++;
// If palindrome centered at i expand past R,
// adjust center based on expanded palindrome.
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
// Find the maximum element in P.
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n-1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
delete[] P;
return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}