求指定字符串的最長迴文子串。
給定 std::string s,採用manacher算法,在O(n)時間內,在O(n)空間下,解決該問題。
字符串包括奇數長的和偶數長,同時其中所含的迴文串也分奇數長和偶數長,如果分情況討論的話,比較複雜。manacher想出了一個方法,可以統一各種情況。
manacher算法依次遍歷各個字符,來計算 以各個字符爲中心的最長迴文串的半徑,並用一個一維數組來保存。
剛瞭解這個算法時,有人會說,因爲要求最長迴文串,不用數組保存就行。只用兩個變量分別存儲目前爲止最長迴文子串的半徑和該中心字符所在的下標即可。這種想法是錯誤的。因爲計算當前的p[i]時,要用到之前已經求出來的值。
下面進入正題。
我們首先要預處理一下字符串,在原始字符串中加入不在該字符集中的一個特殊符號,比如'#'號,來將所有輸入的串變爲奇數長度,同時,以每個字符爲中心的迴文串也變成了奇數長度。
對於長度爲N的原始字符串s,加入'#'號後,新的字符串mStr長度變爲了2*N+1。
std::string mStr; //定義成員變量
void preProcessString(const string& s)
{
int sSize = s.size();
int mSize = 2 * sSize + 1;
mStr.resize(mSize);
for (int i = 0; i < sSize; i++)
{
mStr[2 * i] = '#';
mStr[2 * i + 1] = s[i];
}
mStr[mSize - 1] = '#';
}
定義數組p[i]表示以i爲中心的(包含i這個字符)迴文串半徑長。也就是說,如果以mStr[i]爲中心的最長迴文串只有mStr[i]自己,則p[i] = 1.
顯然,p[0] = 1.
需要提前說明的一點是,由於從前向後掃描新字符串mStr,所以在計算p[i]時,已經計算好了p[0]、p[1]...p[i-1]。
當我們要計算p[i]時,定義maxLen爲在以p[0]、p[1]...p[i-1]爲中心的所有迴文子串中,能延伸到的最右端的位置。
定義maxLen = MAX{ id+p[id]) | id∈[0,i-1] },使maxLen取最大值的最後一個id.
下面分兩種情況來討論maxLen <= i 和 maxLen > i:
一、如果maxLen <= i,也就是i是maxLen區間的最後一個或者根本就不在maxLen區間,那麼令p[i] = 1,之後從i開始,向兩邊逐位擴展,注意兩邊都不能下標越界。
令n= 2*N+1,則有
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
二、如果maxLen > i,也就是i位於maxLen區間內。
令j爲i關於id的對稱點,j = 2*id-i;
此種情況又要分爲三種小情況 來進行分析
1)以j爲中心的迴文子串的最左邊界下標 小於 以id爲中心的迴文子串的最左邊界下標。如下圖所示:
其中p[j]爲圖中藍色部分。這時,p[i] = mx - i + 1; 且p[i]不可能更長。原因是如果p[i]比圖中右邊的綠條更長的話,哪怕只長1位,由對稱關係知id的迴文子串應該向兩邊擴展。
所以p[i]不可能更長。
2)以j爲中心的迴文子串的最左邊界 等於 以id爲中心的迴文子串的最左邊界。還是以上圖爲例,只看綠色的部分。
這時,p[i] = mx - i + 1,並且還有可能增長。
代碼仍然如下:
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
3)以j爲中心的迴文子串的最左邊界 大於 以id爲中心的迴文子串的最左邊界。
其中,p[j]爲圖中左邊綠色的線條,此時,p[i]的長度和p[j]的長度一樣,p[i] = p[j]。且不可能增長了。
把上面兩大種情況和三小種情況合在一起,整理得:
j = 2*id - i;
if(maxLen <= i)
{
p[i] = 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
}
else
{
if(j - p[j] < id - p[id])
{
//j的最左邊界 超出了 id的最左邊界
p[i] = mx - i + 1;
}
else if(j - p[j] > id - p[id])
{
//j的最左邊界在 id半徑內部
p[i] = p[j];
}
else
{
//j的最左邊界 等於 id的最左邊界
p[i] = mx - i + 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
}
}
上面這是求一個p[i]的方法,下面給出求所有p[i]和maxLen及id的方法。
//這裏的mStr爲預處理過之後的字符串
//這裏的maxLen表示的是之前所有的迴文子串所能延伸到的最右的位置
因爲p[0]必爲1,其對應的id必爲0,其所能延伸到最右的位置必爲0 (下標)
int n = mStr.size(), id = 0, maxLen = 0;
std::vector<int> p(n, 1);
int j = 0;
for(int i = 1; i < n - 1; i++)
{
j = 2*id - i;
if(maxLen <= i)
{
p[i] = 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
}
else
{
if(j - p[j] < id - p[id])
{
//j的最左邊界 超出了 id的最左邊界
p[i] = maxLen - i + 1;
}
else if(j - p[j] > id - p[id])
{
//j的最左邊界在 id半徑內部
p[i] = p[j];
}
else
{
//j的最左邊界 等於 id的最左邊界
p[i] = maxLen - i + 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
}
}
//計算id及maxLen
if (i + p[i] > maxLen)
{
maxLen = i + p[i] - 1;
id = i;
}
}
這樣,到目前爲止,p[n]已經求完了。
在表述上精簡一下代碼,注意,時空複雜度並沒有變。
int n = mStr.size(), id = 0, maxLen = 0;
std::vector<int> p(n, 1);
int j = 0;
for(int i = 1; i < n - 1; i++)
{
j = 2*id - i;
if(maxLen > i)
p[i] = std::min(maxLen - i + 1, p[j]);
else
p[i] = 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
//計算id及maxLen
if (i + p[i] > maxLen)
{
maxLen = i + p[i] - 1;
id = i;
}
}
接下來,就是求p[n]中的最大值了。不管裏放在最後這裏單獨求,還是結合進求p數組的時候直接就把最大值給求了。都會額外多花費O(n)的時間。
int retMaxLen = 0, retIndex = 0;
for (int i = 0; i< n; i++)
{
if (p[i] > retMaxLen)
{
retMaxLen = p[i];
retIndex = i;
}
}
retMaxLen--;
int subStart = (retIndex - retMaxLen)/2;
return s.substr(subStart, retMaxLen);
到目前爲止,全部搞定。去leetcode上測試一下。
=============附 錄================
leetcode本題代碼如下:
class Solution {
public:
string longestPalindrome(string& s) {
//採用manacher算法,時間複雜度爲O(n),空間複雜度爲O(n)
if(1 == s.size())
return s;
preProcessString(s);
int n = mStr.size(), id = 0, maxLen = 0;
std::vector<int> p(n, 1);
int j = 0;
for(int i = 1; i < n - 1; i++)
{
j = 2*id - i;
if(maxLen > i)
p[i] = std::min(maxLen - i + 1, p[j]);
else
p[i] = 1;
while (i-p[i] >=0 && i + p[i] < n && mStr[i + p[i]] == mStr[i - p[i]])
p[i]++;
//計算id及maxLen
if (i + p[i] > maxLen)
{
maxLen = i + p[i] - 1;
id = i;
}
}
//遍歷p數組,找到最大的迴文長度
//可以在O(n)時間內找出任意長度的迴文子串
int retMaxLen = 0, retIndex = 0;
for (int i = 0; i< n; i++)
{
if (p[i] > retMaxLen)
{
retMaxLen = p[i];
retIndex = i;
}
}
retMaxLen--;
int subStart = (retIndex - retMaxLen)/2;
return s.substr(subStart, retMaxLen);
}
private:
void preProcessString(const string& s){
int sSize = s.size();
int mSize = 2 * sSize + 1;
mStr.resize(mSize);
for (int i = 0; i < sSize; i++)
{
mStr[2 * i] = '#';
mStr[2 * i + 1] = s[i];
}
mStr[mSize - 1] = '#';
}
string mStr;
};