LeetCode 0005 Longest Palindromic Substring
1. 題目描述
2. Solution 1: [TLE] Brute force
1、思路
兩層迴圈遍歷所有子串,判斷是否為迴文,保留最長的子串。
2、程式碼實現
/* Solution 1: Brute force 兩層迴圈遍歷所有子串,判斷是否為迴文,保留最長的子串。 */ public class Solution1 { public String longestPalindrome(String s) { String result = ""; int maxLen = 0; int n = s.length(); for (int i = 0; i < n; i++) { for (int j = i + 1; j <= n; j++) { String cur = s.substring(i, j); // s[i, j), 故上面的j要取到等號 if (isPalindromic(cur) && cur.length() > maxLen) { result = cur; maxLen = Math.max(maxLen, result.length()); } } } return result; } private boolean isPalindromic(String s) { int n = s.length(); for (int start = 0, end = n - 1; start < end; start++, end--) if (s.charAt(start) != s.charAt(end)) return false; return true; } }
time complexity: 兩層迴圈O(n2),判斷是否迴文O(n),綜合為O(n3)
space complexity: O(1)
提交之後,TLE(Time Limit Exceeded)
3. Solution 2: LCS
3.1 子問題:最長公共子串(Longest Common Substring)
首先,引入問題最長公共子串(Longest Common Substring),原題見 NC127
1、分析: DP
1) 狀態定義 dp[i][j] 表示字串s1中第i個字元和s2中第j個字元所構成的最長公共子串。 2) 初始狀態 dp\[i][j] = {0} 3) 狀態轉移方程 dp\[i][j] = 0, if s1[i] != s2[j] dp\[i][j] = dp\[i-1][j-1] + 1, if s1[i] == s2[j] 狀態轉移方程中出現了 i - 1,要求必須 i>=1,遍歷字串的時候,i從0開始。 故,對上面的公式做個替換 i -> i + 1,匯出 dp\[i+1][j+1] = dp\[i][j] + 1, if s1[i] == s2[j] ****
2、示例
String s1 = "1AB2345CD", s2 = "12345EF", expected = "2345";
手動模擬結果:
3、程式碼實現
/* DP 1) 狀態定義 dp[i][j] 表示字串s1中第i個字元和s2中第j個字元所構成的最長公共子串。 2) 初始狀態 dp[i][j] = {0} 3) 狀態轉移方程 dp[i][j] = 0, if s1[i] != s2[j] dp[i][j] = dp[i-1][j-1] + 1, if s1[i] == s2[j] */ public class Solution { @Test public void test1() { String s1 = "1AB2345CD", s2 = "12345EF", expected = "2345"; assertEquals(expected, LCS(s1, s2)); } /** * longest common substring * * @param s1 string字串 the string * @param s2 string字串 the string * @return string字串 */ public String LCS(String s1, String s2) { // write code here int maxLen = 0; int end = 0; int m = s1.length(), n = s2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (s1.charAt(i) == s2.charAt(j)) { dp[i + 1][j + 1] = dp[i][j] + 1; if (dp[i + 1][j + 1] > maxLen) { maxLen = dp[i + 1][j + 1]; end = i; } } } } return s1.substring(end - maxLen + 1, end + 1); } }
time complexity: 兩層迴圈,O(n^2)
space complexity: 二維陣列,O(n^2)
4、優化,DP使用一維陣列
/*
DP使用一維陣列
*/
public class Solution1 {
public String LCS(String s1, String s2) {
int maxLen = 0;
int end = 0;
int m = s1.length(), n = s2.length();
int[] dp = new int[n + 1];
for (int i = 0; i < m; i++) {
for (int j = n - 1; j >= 0; j--) {
if (s1.charAt(i) == s2.charAt(j)) {
dp[j + 1] = dp[j] + 1;
if (dp[j + 1] > maxLen) {
maxLen = dp[j + 1];
end = i;
}
} else dp[j + 1] = 0;
}
}
return s1.substring(end - maxLen + 1, end + 1);
}
}
time complexity: 兩層迴圈,O(n^2)
space complexity: 一維陣列,O(n )
3.2 用LCS解決當前問題
1、思路
根據迴文的定義,正序讀與倒序讀一樣。故,把輸入字串s做逆置為 rev,求LCS(s, rev)。
例1:
String s = "caba";
String rev = new StringBuilder(s).reverse().toString(); // "abac"
LCS(s, rev) = "aba";
例2:
String s = "abc435cba";
String rev = "abc534cba";
LCS(s, rev) = "abc" || "cba";
顯然這兩個字串都不是迴文串,所以求出最長公共子串後,並不一定是迴文串,還需要判斷該字串倒置前的下標和當前的字串下標是不是一致。
具體地,正確示例
a) dp過程
b) 判斷下標一致
在dp[3][4] = 3時,取得最長公共子串
此時,j = 2, rev[j] = 'a',為倒著讀終點。
由j推匯出順著讀的起點為: idxInSource = n - 1 - j = 4 - 1- 2 = 1。
由dp推匯出的順著讀終點,idxInSource + dp[3][4] - 1 = 1 + 3 - 1 = 3。
實際順著讀終點為 i = 3。
綜上,滿足要求。
錯誤示例
a) dp過程
b) 判斷下標一致
2、程式碼實現
public class Solution2 {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return s;
String rev = new StringBuilder(s).reverse().toString();
int n = s.length();
int[] dp = new int[n + 1];
int maxLen = 0;
int end = 0;
for (int i = 0; i < n; i++) {
for (int j = n - 1; j >= 0; j--) {
if (s.charAt(i) == rev.charAt(j)) {
dp[j + 1] = dp[j] + 1;
if (dp[j + 1] > maxLen) {
int idxInSource = n - 1 - j; // dp推匯出的順讀起點
// dp匯出的順讀終點 實際順讀終點
if (idxInSource + dp[j + 1] - 1 == i) {
maxLen = dp[j + 1];
end = i;
}
}
} else dp[j + 1] = 0;
}
}
return s.substring(end - maxLen + 1, end + 1);
}
}
time complexity: O(n^2)
space complexity: O(n)
4. Solution 3
1、思路: 用DP優化暴力法
Dynamic Programming
- 狀態定義
boolean dp[i][j] = s[i, j] is Palindromic - 狀態初始條件
dp[i][j] = true if len(s[i, j]) == 1 or j == i or j - i == 0;
dp[i][j] = s[i] == s[j] if len(s[i, j]) == 2 or j == (i + 1) or j - i == 1; - 狀態轉移方程
dp[i][j] = s[i] == s[j] && dp[i+1][j-1]
判斷條件整合:
a. 若: j - i == 0 , 則dp[i][j] = (s[i] == s[j]) 恆為true, 1個字元自然迴文
b. 若: j - i == 1 , 則dp[i][j] = s[i] == s[j] 2個字元,首尾相等即可
c. 若: j - i == 2 , 則dp[i][j] = s[i] == s[j] 3個字元,首尾相等即可,中間有1個字元,不打緊
d. 若: j - i > 2 , 則dp[i][j] = s[i] == s[j] && dp[i + 1][j - 1] 4個字元(含),首尾相等,外加中間子串為迴文才行
a. b. c. 判斷條件整合 => s[i] == s[j] && (j - i <= 2)
與d. 合併後 判斷條件整合為一個 => s[i] == s[j] && ( (j - i <= 2) || dp[i + 1][j - 1] )
2、示例
input: s = "babad";
用j遍歷行,i遍歷列,只使用了下三角
3、程式碼實現
public class Solution3 {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return s;
String res = "";
boolean[][] dp = new boolean[s.length()][s.length()];
int max = 0;
for (int j = 0; j < s.length(); j++) {
for (int i = 0; i <= j; i++) {
dp[i][j] = s.charAt(i) == s.charAt(j) &&
((j - i <= 2) || dp[i + 1][j - 1]);
if (dp[i][j] && (j - i + 1 > max)) {
max = j - i + 1;
res = s.substring(i, j + 1); // [i, j+1)
}
} // end of inner for loop
} // end of outer for loop
return res;
}
}
time: O(n^2)
space: O(n^2)
4、優化使用一維陣列
/*
對Solution3的優化,使用一維陣列
*/
public class Solution4 {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return s;
String res = "";
boolean[] dp = new boolean[s.length()];
int n = s.length();
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= i; j--) {
dp[j] = s.charAt(i) == s.charAt(j) &&
((j - i) < 3 || dp[j - 1]);
if (dp[j] && j - i + 1 > res.length())
res = s.substring(i, j + 1); // [i, j+1)
}
}
return res;
}
}
time: O(n^2)
space: O(n)
5. Solution 4
1、思路:中心擴充套件
迴文一定是對稱的,所以可以迴圈選擇一箇中心,進行左右擴充套件,判斷左右字元是否相等。
因為字串長度可能為奇數,也可能是偶數,所以中心可以是字元,也可以是兩個字元間隙。
設字串長度為n,則間隙為n-1,可能的中心有 n+(n-1) = 2n - 1個。
2、程式碼實現
public class Solution5 {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return null;
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int n = Math.max(expandAroundCenter(s, i, i), // 以i為中心擴充套件
expandAroundCenter(s, i, i + 1)); // 以i後面的間隔為中心擴充套件
if (n > end - start) {
start = i - (n - 1) / 2;
end = i + n / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
int l = left, r = right;
while (l >= 0 && r < s.length() &&
s.charAt(l) == s.charAt(r)) {
l--;
r++;
}
return r - l - 1;
}
}
time complexity: O(n^2)
space complexity: O(1)
6. Solution 5: Manacher's Algorithm 馬拉車演算法
馬拉車演算法 Manacher‘s Algorithm 是用來查詢一個字串的最長迴文子串的線性方法,由一個叫Manacher的人在1975年發明的,這個方法的最大貢獻是在於將時間複雜度提升到了線性。
1、思路分析
- Step 1: 預處理
由於迴文分為偶迴文(如,bccb)和奇迴文(如,bcacb),而在處理奇偶問題上會比較繁瑣,這裡使用一個技巧
a.) 在字串首尾及每個字元間都插入一個"#",這樣可以使得原先的奇偶迴文都變為奇迴文;
b.) 接著在首尾兩端各插入"$"和"^",這樣中心擴充套件尋找回文的時候會自動退出迴圈,不需要每次判斷是否越界;
T: 處理後的陣列,P: 從中心擴充套件的長度
首先,用一個數組P儲存從重心擴充套件的字串最多個數,而它剛好也是去掉"#"的原字串的總長度。如,index=6的地方,P[6]=5,所以它是從左邊擴充套件5個字元,相應的右邊也是擴充套件5個字元,也就是"#c#b#c#b#c#"。而去掉#恢復到原來的字串,變成"cbcbc",它的長度剛好也就是5。
- Step 2: 求原始字串下標
用P的下標i減去P[i],再除以2,就是原字串的開頭下標了。
如,P[6] = 5,也就是迴文串的最大長度是5,對應的下標是6,所以原字串的開頭下標是(6-5) / 2 = 0。所以,只需要返回原字串的第0到第(5-1)位就可以了。
Step 3: 求每個P[i]
用C表示迴文串的中心,用R表示迴文串的右邊半徑,所以R=C+P[C]。C和R所對應的迴文串是當前迴圈中R最靠右的迴文串。
考慮求P[i],用i_m表示當前需要求的第i個字元關於C對應的下標。
現在要求P[i],如果是用中心擴充套件法,那就向兩邊擴充套件比對就行了。其實可以利用迴文串C的對稱性。i關於C的對稱點是i_m,P[i_m]=3,所以P[i] 也等於 3。
但是有三種情況將會造成直接賦值為P[i_m]是不正確的,下邊一一討論。
case 1: 超出了R
當要求P[i]的時候,P[i_m]=7,而此時P[i]並不等於7,為什麼呢?因為我們從i開始往後數7個,等於22,已經超過了最右的R,此時不能利用對稱性了,但我們一定可以擴充套件到R的,所以P[i]至少等於R-i=20-15=5,會不會更大呢,我們只需要比較T[R+1]和T[R+1]關於i的對稱點就行了,就像中心擴充套件法一樣一個個擴充套件。
case 2: P[i_m]遇到了原字串的左邊界
此時P[i_m]=1,但是P[i]賦值成1是不正確的,出現這種情況的原因是P[i_m]在擴充套件的時候首先是"#"=="#",之後遇到了"^"和另一個字元比較,一就是到了邊界,才終止迴圈的。而P[i]並沒有遇到邊界,所以我們可以通過中心擴充套件法一步一步向兩邊擴充套件就行了。
case 3: i等於R
此時,先把P[i]賦值為0,然後通過中心擴充套件法一步一步擴充套件。
- Step 3: 考慮C和R的更新
就這樣一步一步的求出每個P[i],當求出的P[i]的右邊界大於當前的R時,就需要更新C和R為當前的迴文串了。因為必須保證i在R裡面,所以一旦有更右邊界的R就要更新R。
此時的P[i]求出了將會是3,P[i]對應的右邊界將是10+3=13,所以大於當前的R,需要把C更新成i的值,也就是10,R更新成13。
2、程式碼實現
package Q0099.Q0005LongestPalindromicSubstring;
public class Solution6 {
public String preProcess(String s) {
int n = s.length();
if (n == 0) return "^$";
StringBuilder ret = new StringBuilder("^");
for (int i = 0; i < n; i++)
ret.append("#").append(s.charAt(i));
ret.append("#$");
return ret.toString();
}
public 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_m = 2 * C - i;
if (R > i) {
P[i] = Math.min(R - i, P[i_m]);
} else {
P[i] = 0; // 等於R的情況
}
// 碰到之前的三種情況,需要利用中心擴充套件法
while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i]))
P[i]++;
// 判斷是否需要更新R
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
// 找出P的最大值
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2; // 最開始講的求原字串下標
return s.substring(start, start + maxLen);
}
}