Longest Palindromic SubString(最長迴文)
最長迴文是指給定一個字符串,找出其中最長的迴文。Longest Palindromic SubString介紹了幾種算法,翻譯記錄一下。
方法1:最長公共子串
將字符串翻轉,然後找出兩個字符串的最長公共子串,這個子串同時也是最長迴文。
這個方法看似正確,其實有缺陷。
如,S=”caba”,S’=”abac”,最長子串是aba,求解正確。
但是,對於S=”abacdfgdcaba”,S’=”abacdgfdcaba”,最長公共子串是abacd,顯然,這個不是迴文。
從中可以看出,錯誤原因是由於非迴文的子串正好是原字符串的一部分。爲了修正這個錯誤,需要在發現最長子串的時候,檢查子串的下標是不是與翻轉後的原始下標一致(原文是we check if the substring’s indices are the same as the reversed substring’s original indices,我的理解是對於最長公共子串,如果是合法的迴文,相對於S的頭部,S’的末尾的偏移位置應是一樣的,如果不一樣,則意味着該子串只是公共子串,並不具備迴文屬性),如果一致,則更新最長迴文,否則跳過該回文。該算法使用動態規劃算法,時間消耗 。詳見Longest Common SubString。該算法使用的是最長後綴算法,比我設想的動態規劃要簡潔許多,需要多複習。
方法2: 暴力破解
最簡單最粗暴的算法,遍歷所有可能,找到一個答案。
複雜性分析:
- 時間複雜度: .假設n是輸入字符串的長度,則共有n(n-1)/2種子串。檢查是否迴文需要花費O(n),總的複雜度就是 。
- 空間複雜度:
方法3: 動態規劃
爲改進暴力破解方案,我們需要首先觀察如何避免迴文檢查過程中的不必要的重複計算。如字符串ababa,如果我們已經知道bab是個迴文,顯然ababa也是一個迴文,因爲左邊和右邊的字符是相同的。
我們定義P(i,j)如下:
因此:
P(i,j) = (P(i+1, j-1) and Si == Sj)
初始條件:
P(i,i) = true
P(i, i+1) = (Si == Si+1)
這是個很直接的DP算法,我們首先初始化一個和兩個字符的迴文,然後計算3個或者更多.
複雜度分析:
- 時間複雜度:
- 空間負雜度:
方法4: 中心擴展(Expand Around Center)
我們可以用常量空間, 的時間來解決這個問題。
我們觀察到迴文總是圍繞某個中心的一個鏡像。因此,迴文可以從某個中心擴展,共有2n-1箇中心。
爲什麼會有2n-1箇中心?因爲中心也可能會在兩個字符中間。如abba,中心就在兩個b中間。
public String longestPalindrome(String s) {
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 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;
}
複雜度分析:
- 時間複雜度:
- 空間複雜度:
方法5: Manacher算法
Manacher算法複雜度是O(n),詳見Manacher’s algorithm. (還沒來的及研究,存檔先)
附上我實現的幾個方法:
版本1: 暴力破解
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
int size = 0;
boolean isVliad = false;
String sub = "";
for (int i = 0; i < s.length(); i++) {
for (int k = size; k < s.length() - i; k++) {
isVliad = true;
for (int x = i,y = i + k; x <= y; x++,y--) {
if (s.charAt(x) == s.charAt(y)) {
System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
} else {
isVliad = false;
break;
}
}
if (isVliad && (k + 1) > size) {
size = k + 1;
sub = s.substring(i, i + k + 1);
System.out.println(String.format("%d----%d:%s", i, k, sub));
}
}
}
System.out.println(sub);
return sub;
}
毫無懸念,提交後直接超時。
版本2: 改進型的暴力破解
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
Map<Character, List<Integer>> charMap = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
Character k = s.charAt(i);
List<Integer> set = charMap.get(k);
if (set == null) {
set = new ArrayList<>();
set.add(i);
charMap.put(k, set);
} else {
set.add(i);
}
}
boolean isValid = false;
int size = 1;
String sub = s.substring(0, 1);
int tail = 0;
for (int i = 0; i < s.length(); i++) {
List<Integer> set = charMap.get(s.charAt(i));
for (int k = set.size() - 1; set.get(k) > i; k--) {
isValid = true;
tail = set.get(k);
if (size > (tail - i + 1)) {
break;
}
for (int x = i,y = tail; x < y; x++,y--) {
if (s.charAt(x) == s.charAt(y)) {
// System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
} else {
isValid = false;
break;
}
}
if (isValid) {
if (size < (tail - i + 1)) {
size = tail - i + 1;
sub = s.substring(i, tail + 1);
System.out.println(String.format("%d----%d:%s", i, k, sub));
}
break;
}
}
}
System.out.println(sub);
return sub;
}
只是用了一些輔助手段,減少了遍歷次數,提交通過,耗時144ms。(當然,我給不出複雜度分析。這是不是從一個側面說明了工程方法可以解決算法問題?。。。)
版本3: 動態規劃算法
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
boolean track[][] = new boolean[s.length()][s.length()];
String longest = s.substring(0, 1);
for (int i = 0; i < s.length(); i++) {
track[i][i] = true;
}
int j = 0;
for (int k = 1; k < s.length(); k++) {
for (int i = 0; i < s.length() - k; i++) {
j = i + k;
if (k == 1 || track[i+1][j-1]) {
if (s.charAt(i) == s.charAt(j)) {
track[i][j] = true;
if (j - i + 1 > longest.length()) {
longest = s.substring(i, j + 1);
}
}
}
}
}
return longest;
}
不負衆望,60ms。
版本4: 最長公共子串
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
StringBuilder sb = new StringBuilder();
for (int i = s.length() - 1; i >= 0; i--) {
sb.append(s.charAt(i));
}
String r = sb.toString();
int track[][] = new int[s.length()][s.length()];
String sub = s.substring(0, 1);
String rsub = "";
String tmp = "";
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j < s.length(); j++) {
if (s.charAt(i) == r.charAt(j)) {
if (i == 0 || j == 0) {
track[i][j] = 1;
} else {
track[i][j] = track[i-1][j-1] + 1;
}
if (track[i][j] > sub.length()) {
int start = i - track[i][j] + 1;
tmp = s.substring(start, i + 1);
rsub = r.substring(r.length() - i - 1, r.length() - start);
// System.out.println(String.format("%d-%d :: %d-%d", start, i + 1, r.length() - i - 1, r.length() - start- 1));
// System.out.println(tmp + "--" + rsub);
if (tmp.equals(rsub)) {
sub = tmp;
System.out.println(sub);
}
}
}
}
}
return sub;
}
有點意外,同樣是 算法,耗時是366ms,比我的改進型暴力破解算法還要多。估計是字符串截取和比較導致多餘的消耗。