Manacher算法
转载自https://www.cnblogs.com/z360/p/6375514.html
manacher算法,我习惯叫他 “马拉车”算法。
在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如abba,noon等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。
计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为O(n3)的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为O(n2),但是当n比较大的时候仍然无法令人满意,Manacher算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
1.Manacher算法原理与实现
下面介绍Manacher算法的原理与步骤。
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:
(1)Len数组简介与性质
Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到回文字串对称中心T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
(r-i+1的意思就是最右字符的下标减去中心字符的下标再 加 1;
举个例子 “abcbd”,回文字串是“bcd”,最右字符的下标是3,中心字符的下标是2,中心到最右字符的长度就是3-2+1 = 2,两个字符长度)
对于上面的例子,可以得出Len[i]数组为:
Len数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度。
至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。
有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。
(2)Len数组的计算
首先从左往右依次计算Len[i],当计算Len[i]时,Len[j] (0<=j<i)已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:
第一种情况:i<=P 那么找到i相对于po的对称位置,设为j。
那么如果Len[j]<P-i+1,如下图:
那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样。由对称性可知Len[i]=Len[j]。
如果Len[j]>P-i+1。
由对称性可知,P右侧的字符与2Po-p左侧的一定不对称(对称的话P的位置就是错的。)
所以以i为中心的回文字串半径长度只有P-i+1,Len[i]=P-i+1。
如果Len[j] = p-i+1。
需要验证P右侧是否对称,只不过是从P位置开始
说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]。
第二种情况: i>P
如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。
2.时间复杂度分析
Manacher算法的时间复杂度分析和Z算法类似,因为算法只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于T字符串中的每一个位置,只进行一次匹配,所以Manacher算法的总体时间复杂度为O(n),其中n为T字符串的长度,由于T的长度事实上是S的两倍,所以时间复杂度依然是线性的。
下面是算法的实现,注意,为了避免更新P的时候导致越界,我们在字符串T的前增加一个特殊字符,比如说‘$’,所以算法中字符串是从1开始的。
class my_String
{
public:
int LongestPalindromeString(string s)
{
string newString = "$#";
cout<<s.length()<<endl;
for(int i = 0;i<s.length();++i)
{
newString += s[i];
newString +='#';
}
// cout<<newString<<endl;
int mx = 0;//当前已经计算了的回文子串的右边界抵达的最大位置值;
int ans = 0;//保存最长回文子串的字符长度半径;
int po = 0;//po是去的mx这个右边界的中心点
vector<int> Len(newString.size(), 0);//用于保存回文子串的半径长度。
for(int i =0;i<newString.length();++i)
{
Len[i] = mx>i?min(mx-i+1,Len[2*po-i]):1; //如果mx>i,也就是i在以po为中心的回文子串内部。
//上面这一步其实是计算i位置已经对称的回文子串的长度(边界),下面就可以直接从边界处继续匹配。
while(newString[i-Len[i]] == newString[i+Len[i]])
{
Len[i]++;//更新当前回文子串的半径,同时也是递增检查下一个位置的字符是否相同
}
if(Len[i]-1+i>mx)//验证mx的位置是否需要更新
{
mx = Len[i]+i;
po = i;//更新最右边界mx的对称点。
}
ans = max(ans,Len[i]);//更新最长回文子串的字符长度半径
}
return ans-1;//因为此处的ans最长半径其实是#加上非#字符的长度。
//在newString中最长的回文子串长度是2*ans - 1;分为(ans)个#和ans-1个非#原字符,所以返回ans-1即可;
}
//Len[i]表示以字符newString[i]为中心的最长回文字串的最右字符到newString[i]的字符长度
//比如以newString[i]为中心的最长回文字串是newString[l,r],那么Len[i]=r-i+1。
};
int main(int argc, char *argv[])
{
cout << "Hello World!" << endl;
my_String s;
cout<<s.LongestPalindromeString("111");
return 0;
}
LeetCode[最长回文子串]解答
class Solution {
public:
string longestPalindrome(string s)
{
string manaStr = "$#";
for (int i=0;i<s.size();i++)
{
manaStr += s[i];
manaStr += '#';
}
vector<int> rd(manaStr.size(), 0);
int pos = 0, mx = 0;
int start = 0, maxLen = 0;
for (int i = 1; i < manaStr.size(); i++)
{
rd[i] = i < mx ? min(rd[2 * pos - i], mx - i) : 1;
while (i+rd[i]<manaStr.size() && i-rd[i]>0 && manaStr[i + rd[i]] == manaStr[i - rd[i]])//这里要注意数组越界的判断,源代码没有注意,release下没有报错
rd[i]++;
if (i + rd[i] > mx) //如果新计算的最右侧端点大于mx,则更新pos和mx
{
pos = i;
mx = i + rd[i];
}
if (rd[i] - 1 > maxLen)
{
start = (i - rd[i]) / 2;
maxLen = rd[i] - 1;
}
}
return s.substr(start, maxLen);
}
};