算法背景
Manacher算法是针对于字符串的一个处理算法,目的在于求解一个字符串的最大回文串。它的时间复杂度为O(n)。求解一个字符串的最大回文串最容易想到的就是从一个字符串出发将其作为中心,依次向两端扩展,直到遇到不同的字符。这种算法的最坏时间复杂度可以达到O(n^2),除此之外对于偶数长度的最大回文串需要另行讨论,因此不是最佳的解法。
算法原理
如上所述,Manacher算法对于偶数长的回文串存在一定的限制。对于这一点,Manacher算法采取的措施是,将原字符串首、尾包括字符之间插入一个标记的字符。这个字符不能在原字符串中出现。(举例,如原字符串为AB,则改变后的字符串为#A#B#)Manacher算法之所以这样做,是因为这样可以将偶数回文串和奇数回文串全部统一为奇数回文串,便于处理。之后我们就可以通过先求新串的结果,再来处理原串以此得到答案。
在KMP算法里,我们知道可以用之前得到的结论来取消某些不必要的遍历来降低时间成本,这里同样采取类似的思路。我们可以维护一个数组p,p[i]的含义是以第i个字符为中心,最大的回文串的半径长度。例如:对于原串ABCB,我们将其处理为#A#B#C#B#,它的p数组为
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
字符 | # | A | # | B | # | C | # | B | # |
p[i] | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 2 | 1 |
那么p数组应该如何维护呢?我们定义一个变量id,令p[id]为已维护的p[i]中的最大值。令mx = id + p[id],即为该最长回文串的右边界。在遍历过程中,会出现如下的两种情况:
1、当前所处的位置i已经超过了mx。此时,由于前面的字符我们并没有接触过,因此只能老老实实从i出发,向两侧扩展,直到字符不同后停止并修改当前p[i]的值。
2、若当前的位置i在mx左侧,即i < mx,则会出现下图的状况:
从这里不难看出,j若为i关于id的对称点,则它的坐标为j = 2*id - i。此时p[i]具有一个最小值,如果对于p[j]来讲,它的最左端没有超出id对应字符串的左边界(就是mx的对称点),则根据对称性,p[i] = p[j]。否则,由于超出id右侧的部分我们并不确定是否对称,因此p[i]有最小值:j - mx对称点。即mx - j。之后回到第一种情况向两边遍历修改p[i]即可。
最后在得到最大值p的最大值most后,我们只需要将(most*2-1)/2(乘2再-1将长度变为新串里最大回文的长度,除以2回到原串最大的长度)就可以得到原串的最大回文串的长度了。
代码实现
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int id,j;
char str[10000],a[10000];
int p[10050];
void getnew()
{
memset(str,0,sizeof(str));
str[0] = '*';//实际应用时,为了方式越界,我们会初始化第一个和最后一个元素,所以在遍历过程中是从1开始的
int len =strlen(a);
j = 1;
for(int i = 0;i<len;i++)//重构字符串
{
str[j++] = '#';
str[j++] = a[i];
}
str[j++] = '#';
str[j++] = '@';//防止越界
}
int algor()
{
memset(p,0,sizeof(p));
id = 0;
int mx = 0;
int most = 0;
for(int i = 1;i < j;i++)
{
if(i < mx)
p[i] = min(p[2 * id - i], mx - i);
else
p[i] = 1;
while(str[i + p[i]] == str[i - p[i]])//依次遍历
p[i] ++;
if(i + p[i] > mx)//更新id和mx
{
id = i;
mx = p[i] + i;
}
if(most < p[i])//寻找p的最大值
most = p[i];
}
return most;
}
int main()
{
while(1)
{
gets(a);
getnew();
int d = algor();
printf("%d\n",(d * 2 - 1) / 2);
}
}