最長回文子串
leetcode-5 - 中等
給你一個字符串 s,找到 s 中最長的回文子串。
參考資料:
經查閱,該題一般有四種解法:
- 暴力法
- 中心擴展法
- 動態規划
- Manacher算法
暴力法
暴力解法思路清晰、編寫簡單,但是時間復雜度高,這里不再深入分析。
-
時間復雜度:O(N3),子串的左右邊界以及判斷子串是否為回文字串。
-
空間復雜度:O(1),只需幾個變量值做記錄即可。
中心擴展法
中心擴展法通過枚舉可能出現的回文子串的“中心位置”,從“中心位置”嘗試盡可能擴散出去,得到一個回文串。
子串的“中心位置”有兩種情況:
-
中心為單個字符
-
中心為兩個字符的間隙
圖 1 :奇數回文串與偶數回文串
因此,字符串的每個字符以及字符之間的間隙都要作為中心進行擴展尋找回文子串。
圖 2:枚舉可能的所有回文中心
- 時間復雜度:O(N2),遍歷字符與間隙為O(N),每個字符或間隙擴展尋找最長回文子串為O(N)
- 空間復雜度:O(1)
class Solution {
String longSubStr = "";
int len = 1;
public String longestPalindrome(String s) {
for (int i = 0; i < s.length(); i++) {
// 查找最長的單中心回文子串
int left = i;
int right = i;
core(s, left, right);
// 查找最長的雙中心回文子串
if (i < s.length()-1) {
left = i;
right = i + 1;
core(s, left, right);
}
}
return longSubStr;
}
private void core(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
if (right - left + 1 >= len) {
len = right - left + 1;
longSubStr = s.substring(left, right+1);
}
left--;
right++;
}
}
}
動態規划法
如果用 f[1][2]保存子串從 i 到 j 是否是回文子串,那么在求 f[i][j] 的時候如果 j-i>=2 時,且 f[i][j] 為回文,那么 f[i+1][j-1],也一定為回文,否則 f[i][j] 不為回文。如下圖:
圖 3:回文子串關系圖
因此得動態轉移方程:
圖 4:回文子串動態轉移方程
- 時間復雜度:O(N2),需要填滿二維 f 數組的一半
- 空間復雜度:O(N2),二維 f 數組占用空間為O(N2)
從動態轉移方程可知,只需要二維數組 f[i][j] (0<=i<=j<s.length()) 的一半來記錄子串從 i 到 j 是否為回文串即可,並且上一行的值又依賴下一行的值,因此二維數組行從下向上推導,代碼如下:
class Solution {
int longestLen = 1;
String longestStr = "";
String longestPalindrome(String s) {
int len = s.length();
boolean[][] dp = new boolean[len][len];
for (int i = len-1; i >= 0; i--) {
for (int j = i; j < len; j++) {
if (s.charAt(i) == s.charAt(j) && (j <= i + 1 || dp[i+1][j-1])) {
dp[i][j] = true;
if (j - i + 1 >= longestLen) {
longestLen = j - i + 1;
longestStr = s.substring(i, j+1);
}
}
}
}
return longestStr;
}
}
說明:
動態規划並非最長回文子串的最佳算法,即便與中心擴展法相比,其空間復雜度也較高。但是,使用動態規划解答最長回文子串,可以較好的理解如何得到動態規划的狀態轉移方程。
Manacher算法
Manacher算法本質上也是中心擴展法,但是Manacher算法巧妙的利用了回文子串的對稱性,使得在確定某些中心位置的最長回文子串時不必再向左右擴展一遍。
預處理:
為了解決回文子串的中心位置可能為單個字符也可能為字符間隙的問題(也即奇偶子串的問題),Manacher需要對子串進行預處理,以保證回文子串的中心位置都是一個具體的字符。具體的做法就是使用特殊字符'#'將字符串的各個字符進行隔離。
圖 5:原始字符串與Manacher字符串的對應關系
Manacher字符串有如下特點:
-
Manacher字符串的回文子串一定是奇數長度,並且子串的兩端字符一定是特殊字符'#'
-
Manacher字符串的任意回文子串在原始字符串中一定能找到唯一的一個回文子串與之對應(撤掉特殊字符'#'即可),因此Manacher字符串的最長回文子串撤掉特殊字符'#'后就是原始字符串的回文子串
-
原始字符串與Manacher字符串字符的下標位置有如圖所示的對應關系
概念:
這里解釋一下Manacher算法的幾個概念:
-
回文半徑和回文直徑:因為處理后回文字符串的長度一定是奇數,所以回文半徑是包括回文中心在內的回文子串的一半的長度,回文直徑則是回文半徑的2倍減1。比如對於字符串 "aba",在字符 'b' 處的回文半徑就是2,回文直徑就是3。
-
最右回文邊界 R:在遍歷字符串時,每個字符遍歷出的最長回文子串都會有個右邊界,而R則是所有已知右邊界中最靠右的位置,也就是說R的值是只增不減的。
-
回文中心 C:取得當前R的第一次更新時的回文中心。由此可見R和C時伴生的。
-
半徑數組:這個數組記錄了原字符串中每一個字符對應的最長回文半徑。
下面尋找Manacher字符串的最長回文子串:
Manacher算法的兩個要點:
-
尋找新字符為中心的最長回文子串,如果新字符在最右回文邊界的右側,則使用中心擴展法
-
如果新字符在最右回文邊界的左側(包含最右回文邊界),則該新字符的最長回文半徑至少等於其關於回文中心 C 對稱的字符的最長回文半徑,此時可以從該回文半徑處繼續對新字符使用中心擴展法
現在對上面的第二點進行分析:
- 如圖所示,由於回文子串的對稱性,當 i' 的回文子串在LR內時,i 的回文子串必與 i' 一樣,因為 i' 回文子串的左右兩側的字符不一樣, i 回文子串左右兩側的字符也一定不一樣。
圖 6:i' 的回文子串在LR內
- 如果 i' 的回文子串超出LR,則 i 的回文子串的回文半徑必然是 i 到 R。如圖,x 和 y 分別是 L 和 L' 的左側和右側的字符,若 x=y,而由於回文字符串的對稱性可知, y=k,若 k=z ,則有 x=z,那么回文中心C的最長回文子串就不可能是LR,因此必然有 k≠z,也即 i 的回文半徑就是 i 到 R。
圖 7:i' 的回文子串超出LR
- 如果 i' 的回文左邊界剛好與 L 重合,則 i 的回文半徑至少是 i 到 R,需要在此基礎上繼續中心擴展。
圖 8:i' 的回文子串左邊界在L處
Manacher算法code
public String longestPalindrome(String s) {
String manacherString = getManacherString(s);
int[] p = new int[manacherString.length()];
int maxIndex = 0;
int max = 0;
int c = -1;
int r = -1;
// 以Manacher字符串的每個字符為中心進行擴展
for (int i = 0; i < p.length; i++) {
// 回文半徑數組,其與回文長度L的關系為L=2*p[i]-1
p[i] = 1;
// 回文子串左邊界,其與回文半徑的關系為left=i-p[i]+1
int left = i - 1;
// 回文子串右邊界,其與回文半徑的關系為right=i+p[i]-1
int right = i + 1;
// 如果回文中心字符不在回文最右邊界的右側,則可以利用半徑數組使回文左右邊界跳躍擴展,但右邊界最大擴展到r
// left和right分別指向回文子串左右邊界的下一個字符
if (i <= r) {
if (i+ p[2*c-i] - 1 < r) {
left = i - p[2*c-i];
right = i + p[2*c-i];
p[i] = p[2*c-i];
} else {
left = 2*i-r-1;
right = r+1;
p[i] = r-i+1;
}
}
// 對回文子串左右邊界進行擴展
while (left >= 0 && right < p.length) {
if (manacherString.charAt(left) == manacherString.charAt(right)) {
p[i] += 1;
left--;
right++;
} else {
break;
}
}
// 更新回文半徑和回文中心
if (right - 1 > r) {
r = right - 1;
c = i;
}
// 更新最長回文子串中心索引
if (p[i] > max) {
maxIndex = i;
max = p[i];
}
}
return manacherString.substring(maxIndex - p[maxIndex] + 1, maxIndex + p[maxIndex] - 1).replace("#", "");
}
private String getManacherString(String s) {
StringBuffer sb = new StringBuffer();
sb.append("#");
for (int i = 0; i < s.length(); i++) {
sb.append(s.charAt(i));
sb.append("#");
}
return sb.toString();
}