最長迴文子串

最長迴文子串（Longest palindromic substring）是計算機科學中的問題，在一個字符串中查找一個最長的連續的迴文的子串，例如“banana”最長迴文子串是“anana”。最長迴文子串並不一定是唯一的，比如在“abracadabra”中，沒有超過3個字符的迴文子串，但是有兩個迴文字串長度都是3：“ada”和“aca”。在一些應用中，我們求出全部的極大迴文子串（不被其他迴文串包含的迴文子串）。
Glenn Manacher 於1975年發現了一種線性時間算法，可以在列出給定字符串中從任意位置開始的所有迴文子串。並且，Apostolico, Breslauer & Galil 發現，同樣的算法也可以在任意位置查找全部極大迴文子串，並且時間複雜度是線性的。因此，他們提供了一種時間複雜度為線性的最長迴文子串解法。另外，Jeuring (1994),  Gusfield (1997)發現了基於後綴樹的算法。也存在已知的高效並行算法。
最長迴文子串算法不應當與最長迴文子序列算法混淆。

**定義**

**迴文字符串**
如果一個長度為  的字符串  當中所有字符依次為 . 且  滿足 , 那麼則稱  為一個迴文字符串。

**最長迴文子串**
如果字符串  的一個迴文子串  是  所有迴文子串中長度最長的，那麼則稱  為  的最長迴文子串。

**暴力循環**
在瞭解Manacher算法之前，我們先了解一下“笨”辦法，也就是暴力循環，或者說死算。
注意到每個迴文字符串都是以一個對稱點為中心左右對稱的，所以暴力運算的方式是我們循環每個字母作對稱點，然後在每個對稱點查看所能找到的最大回文字符串的長度，最終得到結果。

**偶長度迴文字符串的處理**
需要注意到的一點，就是如果單純的把每個字符當作對稱點的話，只能得到奇數長度的迴文字符串符。而回文字符串可能是偶數長度的，比如“cbaabd”中的“baab”，此時對稱點是介於兩個字符“a”中間的。這樣的話，如果直接處理起來，可能會比較麻煩，比如需要額外定義如何表示字符中間的位置。
這裡有一種巧妙的辦法，就是在字符串中插入一些定位符，比如“$”，比如字符串“cbaabd”就會變成“$c$b$a$a$b$d$”。
此時我們只需要按照奇長度字符串的處理方式去處理這個字符串（即每次選取對稱點只需依次選取字符），最後得到的迴文字符串“$b$a$a$b$”中，再把定位符“$”去掉，我們就可以得到原始的偶長度字符串“baab”了。

**偽代碼**
將輸入字符串s插入定位符，比如"$"
定義一個數組Pi，用於存儲以i位置為對稱中心的最長迴文字符串的“半徑”
for c = 0 to len(s) do
// 找到最長迴文字符串“半徑”
r = 0
while c-(r+1) >= 0 and c+(r+1) 

**時間複雜度**
因為我們嵌套循環2次，外循環是n次，內循環平均下來是n/2次，所以時間複雜度是。

**馬拉車算法**
馬拉車算法（Manacher's algorithm）利用迴文字符串和子迴文字符串中觀察到的一些特點，以在線性時間內找出字符串的最長迴文子串。
首先我們觀察一下回文字符串可知，迴文字符串都是對稱的。而且如果一個長迴文字符串的對稱點左面包含一個小的迴文字符串，那麼對稱過去到右面也必然會包含一個小的迴文字符串，比如“dacabacad”這個字符串中，對稱點b左面有一個迴文字符串“aca”，右面也會對稱出一個迴文字符串“aca”。

**迴文字符串邊界的情況討論**
那麼我們觀察對稱點左面出現的這個小回文字符串，這個字符串有三種情況：

**情況1**
如果左側小回文字符串的左邊界在大回文字符串的左邊界之內，那麼右面對稱出的小回文字符串也不會觸碰到大回文字符串的右邊界。
比如“dacaxacad”這個字符串，左側的“aca”沒有超過這個大回文字符串的左邊界，那麼右面對稱出的“aca”也不會超過右邊界。
也就是說，在這種情況下，右面這個小回文字符串的長度與對稱的小回文字符串的長度相等，絕對不會超過這個大回文字符串。

**情況2**
如果左側小回文字符串的左邊界超過了大回文字符串的左邊界，那這個右面對稱出的小回文字符串會正好觸碰到大回文字符串的右邊界，但是不會超出。
比如觀察這個字符串“dcbabcdxdcbabce”。左側的小回文字符串的邊界超出了用下劃線標出的大回文字符串的左邊界。對稱過來的右側的小回文字符串的邊界會剛好卡在大回文字符串的右邊界。這是由於大回文字符串右邊界之外的下一個字母（此處是“e”）絕對不是左邊界的那個字母“d”的對稱，所以右邊的小回文字符串延申到邊界之後也無法繼續延申下去了。
也就是說，在這種情況下，右面這個小回文字符串的右邊界與大回文字符串的右邊界相同，那麼這個小回文字符串的長度也絕對不會超過這個大回文字符串。

**情況3**
如果左側小回文字符串的左邊界正好卡在大回文字符串的左邊界上，那麼右面對稱出的小回文字符串有可能會繼續延伸下去，超過大回文字符串的右邊界。
比如觀察這個字符串“abcdcbabcdxdcbabcdxdcb"，左邊的小回文字符串的左邊界正好卡在大回文字符串的左邊界上，那麼對稱過來的大回文字符串是有可能繼續延申下去的。比如在這個例子中，右面以“a”為對稱點的小回文字符串一直能向右延申到整個字符串的結尾。
也就是說，在這種情況下，右面這個小回文字符串的右邊界至少與大回文字符串的右邊界相同，並且有可能會延申。也就是說這個小回文字符串的長度可能會超過這個大回文字符串。

**總結**
我們考慮了左邊的小回文字符串的邊界與大回文字符串邊界的三種情況，只有情況3，也就是正好邊界在同一位置的時候，右側的小回文字符串的長度才有可能會超過大回文字符串，而其他兩種情況的話，我們可以跳過不去計算。
所以Manacher算法在先找到一個長迴文字符串之後，可以選擇性的跳過很多字母，無需一一計算，與暴力循環相比極大了提升了算法的效率。

**偽代碼**

**時間複雜度**
假設我們每次循環能找到的大回文字符串的長度為m，然後我們可以一直跳過計算，直接從該字符串的右邊界開始繼續循環。如果字符串的總長度為n，那麼我們只需n/m次這樣的循環即可。而確定ｍ的長度需要內循環ｍ次。這樣來說，總時間複雜度是(n/m)*m，也就是。

**實現**

**Python**
注意，以下代碼並未完全按照Manacher算法執行，並未分情況討論，即無論是情況1/2/3中的哪種情況都嘗試向外繼續探測半徑能否延申。
雖然這樣會多幾次無用的嘗試，但是代碼會簡單一些。
另外需要注意的是，這段代碼並非返回最長迴文字符串。
def manacher(s0 : str) -> list:
T = '$#' + '#'.join(s0) + '#@'
l = len(T)
P = 0 * l
R, C = 0, 0
for i in range(1,l-1):
if i  R:
R = Pi + i
C = i
return P

**C++**
constexpr auto MAXN = (int)7000000;
char sMAXN = 0 && i + RLi  maxRight) { pos = i; maxRight = i + RLi; }
		max = std::max(max, RLi - 1);
	} return max;
}

**Matlab**
function m = Manacher(a)
T = '$#' reshape(a;ones(size(a))*'#', 1, ) '@';
l = length(T);
P = zeros(1, l);

mx = 0; %range
id = 0; %center
for i = 2:l-1
if i  mx
mx = P(i) + i;
id = i;
end
end
m = max(P)-1;

**Java**
// 生成新的輔助String, eg: abc123成為#a#b#c#1#2#3#2#1#
public static char manacherStringString(String str) {
char c = str.toCharArray();
char res = new charc.length * 2 + 1;
int index = 0;
for (int i = 0; i != res.length; i++) {
// 兩個一樣效果, 填充符號"#"
resi = (i % 2) == 0 ? '#' : cindex++;
// resi = (i & 1) == 0 ? '#' : cindex++;
}
return res;
}
// 返回最長迴文串長度
public static int maxLcpsLength(String str) {
if (str == null 
str.length() == 0) {
return 0;
}
char charArr = manacherStringString(str);
// 輔助迴文長度數組, 表示最多能擴充多少
int pArr = new intcharArr.length;
// 中心點
int C = -1;
// 最右邊界
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i != charArr.length; i++) {
// i在右邊界內, i`到C的長度和到i到R的距離, 哪個小, 哪個就是起碼成為迴文的區域
// 否則為1, 自身
pArri = R > i ? Math.min(pArr2 * C - i, R - i) : 1;
// 檢查邊界
while (i + pArri  -1) {
if (charArri + pArri == charArri - pArri) {
// 左右字母相等, 擴1, 直到不能擴為止
pArri++;
} else {
// 不能擴
break;
}
}
// 如果大於R, 那更新迴文右邊界和中心點
if ((i + pArri) > R) {
R = i + pArri;
C = i;
}
// 如果需要, 則更新max
max = Math.max(max, pArri);
}
// 返回最大回文長度
return max - 1;
}

**參考資料 ==