问题描述:回文串就是正反读起来就是一样的,如“aba”,“abba”。我们现在讨论的问题是如何求出给定字符串str中最长回文串的长度。(题目链接:最长回文)
学习blog:Manacher’s Algorithm ----马拉车算法
注意:我们所求回文串的长度有奇偶两种情况,并且是给定字符串中连续的子串。
马拉车算法
感觉上面的博客讲得很清楚,然后想自己总结下求解最长回文串的算法
首先,为了省去讨论回文串长度的奇偶性,同时防止讨论过程中数组下标越界的情况,我们对字符串做点手脚:在字符串S的字符之间和S的首尾都插入一个“#”,并在开头加上个非#号字符防止数组下标越界。比如S=“abaa”变为T="$#a#b#a#a#",这样我们就可以只讨论回文串长度为奇数的情况。而在新的字符串中,如:“#a#b#a#”是字符串T中的一个回文串,而在原字符串S中它对应的是aba,aba的长度就是#a#b#a#的半径-1(=4-1)(实际上可以看做长度为n的子串在T中的长度变成了2n-1)
我们令dp[i]表示以第i位为中心点的最长回文串的半径,然后我们试着求数组dp
马拉车算法利用了回文串左右对称的特点,优化了两种情况下dp[i]的求解:
盗一张博主的图ヽ(゚∀゚)メ(゚∀゚)ノ ,其中R表示之前最长回文串能够到达的最右端的点,Mi是R对应的回文串的中心位置。
当 i <=R时,如何计算 P[ i ]的值了?毫无疑问的是数组P中点 i 之前点对应的值都已经计算出来了。利用回文串的特性,我们找到点 i 关于 Mi 的对称点 j ,其值为 j= 2*Mi-i 。因点 j 、i 在以Mi 为中心的最大回文串的范围内([L ,R])
———那么如果P[j] <R-i (同样是L和j 之间的距离),说明,以点 j 为中心的回文串没有超出范围[L ,R],由回文串的特性可知,从左右两端向Mi遍历,两端对应的字符都是相等的。所以P[ j ]=P[ i ](这里得先从点j转到点i 的情况)
———如果P[ j ]>=R-i (即 j 为中心的回文串的最左端超过 L)。即,以点 j为中心的最大回文串的范围已经超出了范围[L ,R] ,这种情况,等式P[ j ]=P[ i ]还成立吗?显然不总是成立的!但由回文串的特性可知,P[ i ] 至少等于R- i,至于是否大于R-i(图中红色的部分),我们还要从R+1开始一一的匹配,直达失配为止,从而更新R和对应的Mi以及P[ i ]。
当 i > R时,没法利用到回文串的特性,只能老老实实的一步步去匹配。
从这里也可以看出,我们维护的R值要越靠右端越好。
代码实现:
char str[manx];
int dp[manx];
int main()
{
while(scanf("%s",str+1)!=EOF)
{
int len=strlen(str+1);
str[0]='$',str[2*len+1]='#';
for(int i=len; i>=1; i--)
{
str[i*2]=str[i];
str[i*2-1]='#';
}
int pos=1,ans=1;
dp[0]=dp[1]=0;
for(int i=2; i<=2*len; i++)
{
if(i<=pos+dp[pos])
{
int L=pos-dp[pos];
int j=pos-(i-pos);
int l=j-L;
if(dp[j]<l)
dp[i]=dp[j];
else
{
while(str[i-(l+1)]==str[i+(l+1)])
l++;
dp[i]=l;
}
}
else
{
int l=0;
while(str[i-(l+1)]==str[i+(l+1)])
l++;
dp[i]=l;
}
if(dp[i]+i>dp[pos]+pos)
pos=i;
ans=max(ans,dp[i]);
}
printf("%d\n",ans);
}
}
变型例题:HDU 4513 吉哥系列故事――完美队形II
它区别于“吉哥系列故事――完美队形I”中所求的子串可以是不连续的,而且他们讨论的是两种不同的算法.
马拉车算法复杂度简单分析
上述的算法实现过程中,有两个值R和i(处理的字符串长度是2n+1),其中i会从第一位遍历到最后一位,操作次数为2n+1。在i遍历的期间,注意:因为R记录的是之前最长回文串能够到达的最右端的位置,只有当“i<=R且P[ j ]>=R-i时”或“当 i > R时”,R才会更新并逐一增加,所以基于R的操作次数应该是 < < < 2n+1。当i遍历到最后一位时结束,所以,整体的复杂度应该是O(n)级别的。
最后再总结一下复杂度大于“马拉车”O(n)的几种思路
其中有两种复杂度是O(n^2),还有一种是O(nlogn)
- O(n^2):直接暴力枚举回文串的中心点,往两边遍历求长度。最坏的情况就是字符串中所有字母都相同时,每次都会遍历到边界。
- O(n^2):如果已知字符串中str[i]到str[j]是一个回文串,那么当str[i-1]=str[j+1]时,str[i+1]到str[j+1]也是一个回文子串。利用这个性质,我们可以先判断出所有长度为1、2的连续子串是否为回文串,再判断所有长度为3的连续子串是否为回文串,再判断所有长度为4的连续子串是否为回文串……直到字符串中所有的连续子串都判断完
- O(nlogn):这个思路借助的字符串hash的做法(字符串hash可以求出n个字符串中不同字符串的个数,听说比用set和map快一点点,还可以求两个字符串的最长公共连续子串),字符串hash把字符串str映射成一个整数,使得这个整数尽可能唯一地代表字符串str:它将每一个字符串看做一个26进制的数(或更高进制),然后把它转化成10进制并对一个足够大素数(如1000000007)取模,因为当mod为素数时,冲突的概率会很小。在求解最长回文串中,我们将字符串str中所有连续子串的hash值存起来,再将字符串反转,存下所有子串的hash值。最后枚举回文串的中心点,二分子串的半径求出以当前点为中心时的最长回文串的长度。