求最长回文串(Manacher算法)

普通求回文串方法分析:

我们在平常做题的时候呢,有时候会遇到需要求最长回文串的要求,回文串就是从左读和从右读完全一样的字符串,如 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;
}

这样就找出了最长的回文串,然后取个最大值即可,但是呢,这种方法,你去算一下时间复杂度的话,会发现,代码臭长,而且效率不高,需要跑O(n^{2})的时间,这样如果需要去做其他操作,或者就算不做,题卡时间的话,这种方法也是过不了的,所以我们就引入了“马拉车算法”...

Manacher算法分析:

Manacher算法,有两个核心步骤:

数组扩张:

首先,因为回文串的奇偶问题,我们把它们全部转化成奇数个的问题来解决,具体操作步骤呢,就是用 '#' 夹在每两个字符之间,头尾也要有,而且为了防止越界,在头部第一个位置,用 ‘@’来标记,这样将本来的字符串,扩成了“@#A#.....#”,这样的话,假设原来字符串的长度为 len ,那现在的长度就是 newlen = 2 * len + 2,下面举个例子来证明一下:

位置0123456789101112131415
原串ababcac         
新串@#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[i] 表示以当前字符为中心,得到的最长的回文串的半径,最小值就是只有一个字符,算上它本身,1/2 向上取整也就是 1;因为是在新串里面求的 p 数组,所以不管在原数组中这个回文串的长度是奇是偶,在新串中都是奇数,也就是 2 * p[i] - 1

位置0123456789101112131415
新数组@#a#b#a#b#c#a#c#
p[i]1121414121214121

那 p 数组该怎么求呢?如果还用原来的方法,岂不是又回到起点了?

假设为上一个判断的最长回文串的右界,pos 为上一个最长回文串的中心位置,则 r = pos + p[pos]i 为当前位置,那这里分两种情况:

1. i + p[i] < r

此时的 p[i] = p[ 2 * pos - i ],但是有一种特殊情况,如果 i 关于 pos 的对称点的 值,也就是 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 ] 赋为 即可;

然后以当前位置为中心,以 p[i] 为半径,再向外扩,看还能不能使回文串变得更长;判断出以后,则需要判断是否要更新 r 的值。

好了,求完 p 数组以后,就是重点啦,重点是,如何利用 p 数组来知道这个字符串在原来的数组中的长度,也就是 ans 的值;其实呢,这个字符串的长度是 len = 2 * maxn - 1maxn就是 数组的最大值;然后呢,我们需要把数组中的 ‘#’ 给去掉,其实这个字符串里面的 ‘#’ 的个数,就是 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 - 3068POJ - 3974

 

OVER!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值