普通求回文串方法分析:
我们在平常做题的时候呢,有时候会遇到需要求最长回文串的要求,回文串就是从左读和从右读完全一样的字符串,如 aaaa , ababa, baab, 这样类型的字符串,我们初始的比较省时省力的方法呢,就是从中间往两边扩,但是因为回文串有奇偶之分,所以判断回文串的时候,需要用两种方法来判断,一种判断奇数个的时候的回文串,一种判断偶数个的时候的回文串,所以,我用两个函数来解决,分别是从单个位置往两边扩张去找奇数最长回文,和从两个位置往两边扩张去找偶数最长回文:
int judge(int i) ///奇数个
{
int aa = 0, x = 0;
for(i = i; i + x < len && i - x >= 0; x++) ///控制范围和溢出
{
if(arr[i - x] == arr[i + x])
aa += 2;
else
break;
}
return aa - 1;
}
int judge1(int i, int j) ///偶数个
{
int aa = 1;
if(arr[i] == arr[j])
{
aa = 2;
if(j == len - 1)
return aa;
i--, j++;
for(; i >= 0 && j < len; i--, j++) ///控制范围和溢出
{
if(arr[i] == arr[j])
aa += 2;
}
return aa;
}
else
return aa;
}
这样就找出了最长的回文串,然后取个最大值即可,但是呢,这种方法,你去算一下时间复杂度的话,会发现,代码臭长,而且效率不高,需要跑的时间,这样如果需要去做其他操作,或者就算不做,题卡时间的话,这种方法也是过不了的,所以我们就引入了“马拉车算法”...
Manacher算法分析:
Manacher算法,有两个核心步骤:
数组扩张:
首先,因为回文串的奇偶问题,我们把它们全部转化成奇数个的问题来解决,具体操作步骤呢,就是用 '#' 夹在每两个字符之间,头尾也要有,而且为了防止越界,在头部第一个位置,用 ‘@’来标记,这样将本来的字符串,扩成了“@#A#.....#”,这样的话,假设原来字符串的长度为 len ,那现在的长度就是 newlen = 2 * len + 2,下面举个例子来证明一下:
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
原串 | a | b | a | b | c | a | c | |||||||||
新串 | @ | # | a | # | b | # | a | # | b | # | c | # | a | # | c | # |
原长度 len = 7,新长度 newlen = 2 * 7 + 2 = 16;
代码如下:
void addchar()
{
int i = 0, j = 0;
arr[j++] = '@';
while(j < 2 * len)
{
arr[j++] = '#';
arr[j++] = str[i++];
}
arr[j] = '#';
len = 2 * len + 2; ///len更新成2 * len + 2
}
这样第一步就完成了,下面是第二步:
求 p 数组:
p 数组就是在新串中,最长回文串长度的一半(向上取整),则 p[i] 表示以当前字符为中心,得到的最长的回文串的半径,最小值就是只有一个字符,算上它本身,1/2 向上取整也就是 1;因为是在新串里面求的 p 数组,所以不管在原数组中这个回文串的长度是奇是偶,在新串中都是奇数,也就是 2 * p[i] - 1;
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
新数组 | @ | # | a | # | b | # | a | # | b | # | c | # | a | # | c | # |
p[i] | 1 | 1 | 2 | 1 | 4 | 1 | 4 | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 2 | 1 |
那 p 数组该怎么求呢?如果还用原来的方法,岂不是又回到起点了?
假设 r 为上一个判断的最长回文串的右界,pos 为上一个最长回文串的中心位置,则 r = pos + p[pos] ,i 为当前位置,那这里分两种情况:
1. i + p[i] < r
此时的 p[i] = p[ 2 * pos - i ],但是有一种特殊情况,如果 i 关于 pos 的对称点的 p 值,也就是 p[ 2 * pos - i ] ;
如果( 2 * pos - i ,以这个点为中心的最长回文串的左界) 2 * pos - i + p[ 2 * pos - i ] > pos ;也就是超出了r的范围怎么办呢?因为后面的字符还是未知的,所以不能判断是跟前面完全一样,故此时的p[i]应该是:
p[ i ] = min( p[ 2 * pos - i ], r - i );
2. i + p[i] >= r
这就比较容易了,直接把 p[ i ] 赋为 1 即可;
然后以当前位置为中心,以 p[i] 为半径,再向外扩,看还能不能使回文串变得更长;判断出以后,则需要判断是否要更新 r 的值。
好了,求完 p 数组以后,就是重点啦,重点是,如何利用 p 数组来知道这个字符串在原来的数组中的长度,也就是 ans 的值;其实呢,这个字符串的长度是 len = 2 * maxn - 1;maxn就是 p 数组的最大值;然后呢,我们需要把数组中的 ‘#’ 给去掉,其实这个字符串里面的 ‘#’ 的个数,就是 maxn;然后结果就是 ans = maxn - 1;
上面就是Manacher算法的思想了,下面给出我写的代码...
代码篇:
void Manacher()
{
pos = r = ans = 0;
for(int i = 0; i < len; i++)
{
if(i < r)
p[i] = min(p[2 * pos - i], r - i);
else
p[i] = 1;
while(arr[i + p[i]] == arr[i - p[i]]) ///从当前位置当前半径向外扩张
p[i]++;
if(i + p[i] > r) ///更新右界
{
r = i + p[i];
pos = i;
}
ans = max(ans, p[i] - 1);
}
}
到这,Manacher算法的讲解就结束啦,希望能给点个赞哦!谢谢啦~
例题博客:HDU - 3068,POJ - 3974
OVER!