要解決的問題
求一個字串最長迴文子串是什麼。且時間複雜度
O(N)
具體描述可參考:
暴力解法
以每個字元為中心向左右兩邊擴,直到擴不動為止,記錄下每個字元對應能擴的範圍大小。因為有每個位置左右兩邊能擴的最大範圍,我們可以很方便還原出最長迴文子串是什麼。
比如:AB1234321CD
這個字串,以4
字元為中心向左右兩邊能擴的位置最大,1234321
為最長迴文子串。
如上解法有個問題,即針對類似1ABBA2
這樣的字串,如上演算法會錯過最長迴文子串ABBA
, 因為ABBA
不是以任何一個字串向左右兩邊擴散得到的。所以,需要預處理一下原始字串,預處理的方式如下:
在字串的每兩個位置之間插入一個特殊字元,變成一個預處理後的字串,比如我們可以以#
作為特殊字元(特殊字元選哪個無所謂,不必非要是原始串中不含有的字元),將1ABBA2
這個字串預處理成1#A#B#B#A#2
,用預處理串來跑這個暴力解法,會得到#A#B#B#A#
這個是預處理串的最長迴文子串,我們可以很方便把這個串還原成原始串的最長迴文子串。
暴力解法時間複雜度為O(N^2)
。
Manacher演算法
Manacher演算法可以用O(N)
時間複雜度解決這個問題。同樣的,Manacher演算法也需要對原始字串進行上述的預處理過程。
相關變數說明
pArr
一個整型陣列,長度和預處理串一樣,存每個位置的最長迴文半徑是多少。
比如#A#B#B#A#
這個字串,
位於陣列2號位置的A
的迴文半徑是A#
或者#A
, 長度為2,則pArr[2] = 2
,
位於陣列4號位置的#
的迴文半徑是#B#A#
或者#A#B#
, 長度為5, 則pArr[4] = 5
其他位置以此類推。
通過pArr
的定義,我們顯然可以得到如下結論
pArr[0] = 1
i
整型,當前遍歷到的位置,因為
pArr[0]=1
, 所以i可以從1開始遍歷。
r
整型,迴文最右邊界,只要某個位置能擴到超過這個位置,就更新r這個值,初始值為0,因為一個字串迴文字串至少是1,可以以第0個字元為中心且以0為最右邊界(即:第0個字元本身作為一個迴文串)
c
整型,就是擴到r位置的的中心點,即
pArr[c] = r - c + 1
,初始值為0,與r的初始值定為0一樣的考慮。
流程
考慮i
, r
, c
三個變數之間的位置關係,無非有以下兩種情況
情況1. i
在r
外,比如初始狀態下:i=1, r,c = 0
情況2. i
在r
內或者i==r
關於情況1,流程如暴力解法一樣,以i
位置為中心,左右兩邊擴到不能再擴的位置,更新pArr[i],c, r
的值。
關於情況2,我們假設i'
為i
關於c
對稱的點,r'
為r
關於c
對稱的點,示例圖如下:
細分如下幾種情況:
情況2-1
i'
自己的迴文區域都在[r'...r]
內。
例如下圖中[6...10]
為i'
的最長迴文區域,左邊界並未超過r'
由此可以推出,由於i
位置和i'
位置是關於c
位置對稱的,則i
位置的迴文區域至少包括[14...19]
這一段,如下圖
即pArr[i']
至少等於pArr[i]
,接下來考慮i
能否繼續擴散,即考慮19
位置的值是否等於13
位置的值,
我們可以假設19
位置的值和13
位置的值相等,同時,有如下兩個顯而易見的結論
19
位置的值等於5
位置的值。13
位置的值等於11
位置的值。
推出5
位置的值和11
位置的值相等,那麼由於我們前面假設i'只能擴散到最左6
位置以及最右10
位置,所以,推出的結論和我們的假設矛盾,所以,19
位置的值不等於13
位置的值
所以情況2-1的結論是:i
的最長迴文區域長度和i'
的答案一樣, 即:pArr[i'] = pArr[i]
情況2-2
i'
自己的迴文區域在[r'...r]
外
如下圖
其中[2...14]
範圍是以i'
為中心的最長迴文區域。
在情況2-2下,我們可以得到如下幾個結論:
根據
i
和i'
的關係,以i
為中心,從[13...19]
至少是迴文的。根據
i'
的迴文區域,12
位置的值等於4
位置的值,以c
為中心,4
位置的值又等於20
位置的值,所以12
位置的值等於20
位置的值,即以i
為中心,最長迴文區域還可以擴充套件到[12...20]
。根據
i'
的迴文區域,13
位置的值等於3
位置的值,以c
為中心,13
位置的值又等於11
位置的值,3
位置的值等於21
位置上的值,所以11
位置的值等於21
位置的值,即以i
為中心,最長迴文區域還可以擴充套件到[11...21]
。繼續判斷以
i
為中心,是否可以繼續擴散,即要繼續判斷10
位置的值是否等於22
位置的值,我們假設10
位置的值等於22
位置的值,以c
為中心,10
位置的值等於14
位置的值,以i'
為中心,14
位置的值等於2
位置的值,所以10
位置的值等於2
位置的值,根據我們的假設,2
位置的值會等於22
位置的值。這個與我們的前提矛盾了,因為我們的前提是c
只能擴充套件到[3...21]
這個區域,即:2
位置的值不可能等於22
位置的值,所以我們的假設不成立,所以10
位置的值不等於22
位置的值。
所以,情況2-2的結論是:i
到r
的距離就是i
的迴文半徑,即:pArr[i] = r - i + 1
情況2-3
i'
自己的迴文區域左邊界和r'
壓線
如下圖
其中[3...13]
區域為以i'
為中心能擴的最大回文區域。
有了情況2-2的鋪墊,i
在情況2-3條件下至少可以擴充的範圍是[11...21]
, 但是接下來是否可以繼續擴充,還需要逐個判斷。
自此,所有情況考慮完畢。
由於i在遍歷過程中,始終不回退,所以,Manacher演算法時間複雜度O(N)
完整程式碼
public class LeetCode_0005_LongestPalindromicSubstring {
public static String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
char[] str = s.toCharArray();
char[] strs = manacherStr(str);
int[] pArr = new int[strs.length];
int c = 0;
int r = 0;
int i = 1;
int len = strs.length;
int max = 1;
while (i < len) {
// pArr[i] 至少不需要擴的大小
pArr[i] = i < r ? Math.min(r - i, pArr[c - (i - c)]) : 1;
// 暴力擴
while (i + pArr[i] < len && i - pArr[i] >= 0) {
if (strs[i + pArr[i]] == strs[i - pArr[i]]) {
pArr[i]++;
} else {
break;
}
}
// 擴散的位置能否更新迴文有邊界R
// 如果可以更新,則更新R,且把C置於當前的i,因為是當前的i讓迴文右邊界擴散的
if (i + pArr[i] > r) {
r = i + pArr[i];
c = i;
}
max = Math.max(pArr[i++], max);
}
// 定位最大回文有邊界的迴文中心是哪個
int n = 0;
for (; n < len; n++) {
if (pArr[n] == max) {
break;
}
}
// 構造最大回文子串
StringBuilder sb = new StringBuilder();
for (i = n - max + 2; i < n + max; i += 2) {
sb.append(strs[i]);
}
return sb.toString();
}
public static char[] manacherStr(char[] str) {
char[] strs = new char[str.length << 1 | 1];
for (int i = 0; i < strs.length; i++) {
strs[i] = ((i & 1) == 1) ? str[i >> 1] : '#';
}
return strs;
}
}
相關習題
LeetCode_0005_LongestPalindromicSubstring
LintCode_0200_LongestPalindromicSubstring
LeetCode_0647_PalindromicSubstrings
LeetCode_0214_ShortestPalindrome