求指定字符串的最长回文子串。
给定 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;
};