马拉车 manacher 算法学习笔记

一、目的

给定一个长度为 n n n 的字符串,manacher 以 O ( n ) O(n) O(n) 的时间、空间复杂度,对于此字符串的各位置下标 p 1 p1 p1 求出以其为中心的最长的回文子串的半径(于是可求出此回文子串长度),其中,若此回文子串的左右端点的下标为 [ p 1 − h a l f , p 1 + h a l f ] [p1-half , p1+half] [p1half,p1+half] 则称 h a l f half half 为半径;最终得到整个字符串的半径数组 r a d i u s [ ] radius[] radius[],其中 r a d i u s [ p 1 ] radius[p1] radius[p1] 值为 p 1 p1 p1 处的半径。

二、预处理

若回文子串长度为偶数,则不方便表示其中点、半径等概念,因此先对原给定字符串 s 1 s1 s1 进行预处理,在每个字符之间插入一个特殊字符(应选用原串中不可能出现的某个字符),这样,任意的回文子串的长度都必将是奇数,且所得的回文子串与原字符串 s 1 s1 s1 中的回文子串也有清晰的对应关系。

例:abcde ⇒ \Rightarrow #a#b#c#d#e#

这一技巧(大概?)不仅限于 manacher 算法需要用到,任意回文子串类问题都可考虑使用此技巧。

三、定义与流程

半径与半径数组:略。详见简介部分。

此算法类似 dp,利用前面位置已求得的半径( r a d i u s [ 0 , . . . , p 1 − 1 ] radius[0, ... , p1-1] radius[0,...,p11])以及几个特殊值(接下来要介绍的 R , L , C R,L,C R,L,C),从前往后求出各位置的半径。

假设我们当前已求出 [ 0 , p 1 − 1 ] [0,p1-1] [0,p11] 各下标处的半径值(即以各处为中心的最长回文子串的半径),并维护

  • R = max ⁡ i = 0 p 1 − 1 ( i + r a d i u s [ i ] ) R = \max_{i=0}^{p1-1} (i+radius[i]) R=maxi=0p11(i+radius[i]) :当前已找到的所有最长回文子串的右端所能到达的最右的位置;
  • C C C :取得上述 R R R 的最值时 i i i 的最小值,于是 R = C + r a d i u s [ C ] , C < p 1 R=C+radius[C],C<p1 R=C+radius[C],C<p1
  • L = C − r a d i u s [ C ] L=C-radius[C] L=Cradius[C]

现在我们想要求 p 1 p1 p1 处的半径 r a d i u s [ p 1 ] radius[p1] radius[p1]。设

  • p 2 p2 p2 p 1 p1 p1 关于 C C C 的对称点,于是 p 2 = 2 ∗ C − p 1 p2=2*C-p1 p2=2Cp1​;
  • p 2 L p2L p2L:以 p 2 p2 p2 处为中点的最长回文子串的左端下标,于是 p 2 L = p 2 − r a d i u s [ p 2 ] p2L=p2-radius[p2] p2L=p2radius[p2]

在这里插入图片描述

接下来求 r a d i u s [ p 1 ] radius[p1] radius[p1] 并维护 R , C , L R,C,L R,C,L 的值,就要充分利用 C C C 这个中心。有四种可能情况:

  • a. p 1 > R p1>R p1>R
  • b. p 1 ≤ R , p 2 L > L p1\leq R,p2L>L p1R,p2L>L
  • c. p 1 ≤ R , p 2 L < L p1\leq R,p2L<L p1R,p2L<L
  • d. p 1 ≤ R , p 2 L = L p1\leq R,p2L=L p1R,p2L=L

上图中分别对各情况进行举例,每一行为一例。其中 c. d. 两种情况分别对 p 1 < R p1<R p1<R p 1 = R p1=R p1=R 两种子情况进行举例,但实际上本质相同。上图仅供示意,其各情况(各行)之间显然是相互矛盾的(对应不同情况)。

a. p 1 > R p1>R p1>R:暴力扩展

此情况下, [ L , R ] [L,R] [L,R] 这一回文子串失去利用价值,只能暴力向左右试探性地不断拓展。

特别地,在最开始求 r a d i u s [ 0 ] radius[0] radius[0] 时( p 1 = 0 p1=0 p1=0),我们也希望进入此情况,因此 R R R 的初始值应设为 − 1 -1 1

b. p 1 ≤ R , p 2 L > L p1\leq R, p2L>L p1R,p2L>L r a d i u s [ p 1 ] = r a d i u s [ p 2 ] radius[p1]=radius[p2] radius[p1]=radius[p2]

一方面,以 p 2 p2 p2 为中心、 r a d i u s [ p 2 ] radius[p2] radius[p2] 为半径的回文子串落在了已知回文的 [ L , R ] [L,R] [L,R] 内,则可根据 [ L , R ] [L,R] [L,R] 的对称性知以 p 1 p1 p1 为中心的最长回文子串的半径至少也有 r a d i u s [ p 2 ] radius[p2] radius[p2],即 r a d i u s [ p 1 ] ≥ r a d i u s [ p 2 ] radius[p1]\geq radius[p2] radius[p1]radius[p2];另一方面,假设 r a d i u s [ p 1 ] > r a d i u s [ p 2 ] radius[p1]>radius[p2] radius[p1]>radius[p2],由于 ( p 1 + r a d i u s [ p 1 ] + 1 ) (p1+radius[p1]+1) (p1+radius[p1]+1) 处仍处于 [ L , R ] [L,R] [L,R] 内,则可根据 [ L , R ] [L,R] [L,R] 的对称性知左边的以 p 2 p2 p2 为中心的最长回文子串仍可继续伸长,矛盾,因此 r a d i u s [ p 1 ] ≤ r a d i u s [ p 2 ] radius[p1]\leq radius[p2] radius[p1]radius[p2]。综上所述, r a d i u s [ p 1 ] = r a d i u s [ p 2 ] radius[p1]=radius[p2] radius[p1]=radius[p2]

(实际上,由于 L < p 2 L ≤ p 2 L<p2L\leq p2 L<p2Lp2,根据对称性得 p 1 < R p1<R p1<R 而不可能取等号。)

c. p 1 ≤ R , p 2 L < L p1\leq R,p2L<L p1R,p2L<L r a d i u s [ p 1 ] = R − p 1 radius[p1]=R-p1 radius[p1]=Rp1

一方面,以 p 2 p2 p2 为中心、 r a d i u s [ p 2 ] radius[p2] radius[p2] 为半径的回文子串的左端超出了 [ L , R ] [L,R] [L,R] 的范围,则只有 [ L , 2 ∗ p 2 − L ] [L,2*p2-L] [L,2p2L] 能够通过 [ L , R ] [L,R] [L,R] 对称性对称到右边,得到 [ 2 ∗ p 1 − R , R ] [2*p1-R,R] [2p1R,R] 为回文字串,即 r a d i u s [ p 1 ] ≥ R − p 1 radius[p1]\geq R-p1 radius[p1]Rp1;另一方面,假设 r a d i u s [ p 1 ] > R − p 1 radius[p1]>R-p1 radius[p1]>Rp1,经过一系列对称可得 ( R + 1 ) (R+1) (R+1) ( L − 1 ) (L-1) (L1) 处字符相同,则以 C C C 为中心的最长回文子串仍可继续伸长,矛盾,因此 r a d i u s [ p 1 ] ≤ R − p 1 radius[p1]\leq R-p1 radius[p1]Rp1。综上所述, r a d i u s [ p 1 ] = R − p 1 radius[p1]=R-p1 radius[p1]=Rp1

d. p 1 ≤ R , p 2 L = L p1\leq R,p2L=L p1R,p2L=L:暴力扩展

上述 b. c. 情况在证明 r a d i u s [ p 1 ] radius[p1] radius[p1] 上界时,我们利用矛盾来进行反证,其中

  • b. 情况是与以 p 2 p2 p2 为中心的最长回文子串的矛盾,其中也利用了以 C C C 为中心的最长回文子串的对称性;
  • c. 情况是与以 C C C 为中心的最长回文子串的矛盾,其中也利用了以 p 2 p2 p2 为中心的最长回文子串的对称性;

然而,当 p 2 L = L p2L=L p2L=L 时,上述矛盾均无法构造。因此只能暴力向左右试探性地不断拓展。注意,此时已知 r a d i u s [ p 1 ] ≥ R − p 1 radius[p1]\geq R-p1 radius[p1]Rp1,无需从 p 1 p1 p1 处开始向左右扩展。

上述四种情况中,b. c. 所得的新的最长回文子串右端都没超过 R R R,于是不用更新 R , C R,C R,C;只有 a. d. 情况的暴力扩展需要更新。

上述每次暴力扩展(a. d. 情况)的复杂度都是 O ( R 2 − R 1 ) O(R_2-R_1) O(R2R1),其中 R 1 R_1 R1 为原来的 R R R 值、 R 2 R_2 R2 为更新后的 R R R 值( R 2 > R 1 R_2>R_1 R2>R1);每次 b. c. 情况计算的复杂度都是 O ( 1 ) O(1) O(1)。那么,从前往后处理得到 r a d i u s [ ] radius[] radius[] 数组的总复杂度就是 O ( n ) O(n) O(n),其中 n n n 为原字符串(或预处理后字符串,反正只差 2 2 2 倍的常数)长度。

四、参考代码

#include<cstdio>

char s1[200005];  // 输入的字符串

char s2[400009];  // 预处理后的字符串,大小开2倍!
int radius[400009];  // 开2倍!

void manacher(){
    
    // 预处理
    int i;
    for(i=0 ; s1[i]>0 ; ++i){
        s2[i*2] = '#';  // 注意选用原串中不可能出现的某个字符
        s2[i*2+1] = s1[i];
    }
    s2[i*2] = '#';  // 注意选用原串中不可能出现的某个字符
    s2[i*2+1] = 0;
    int len=i*2+1;

    // manacher
    int R=-1, C=-1;
    for(int p1=0 ; p1<len ; ++p1){
        if(p1>R){  // 情况a
            int half = 0;
            while(p1+half+1<len && p1-half-1>=0 && s2[p1+half+1]==s2[p1-half-1]) ++half;
            radius[p1] = half;  // [p1-half , p1+half] is palindromic
            R = p1+half;
            C = p1;
        }else{
            int L = 2*C-R;
            int p2 = 2*C-p1;
            int p2L = p2-radius[p2];
            if(p2L > L){  // 情况b
                radius[p1] = radius[p2];
            }else if(p2L < L){  // 情况c
                radius[p1] = R-p1;
            }else{  // 情况d
                int half = R-p1;  // 不需要从0开始找
                while(p1+half+1<len && p1-half-1>=0 && s2[p1+half+1]==s2[p1-half-1]) ++half;
                radius[p1] = half;  // [p1-half , p1+half] is palindromic
                R = p1+half;
                C = p1;
            }
        }
    }
}

int main(){
    while(scanf("%s" , s1)>0){
        manacher();
        //for(int i=0 ; s2[i]>0 ; ++i) printf("i=%d : %c , %d\n" , i , s2[i] , radius[i]);

        int max_ra=-1, max_pos=-1;  // 找出最大半径及其第一次出现的位置
        for(int i=0 ; s2[i]>0 ; ++i) {
            if(max_ra < radius[i]){
                max_ra = radius[i];
                max_pos = i;
            }
        }
        
        if(max_ra<=1){
            printf("No solution!\n");
        }else{
            int L = (max_pos - radius[max_pos])/2;
            int R = (max_pos + radius[max_pos])/2 - 1;
            printf("第一个最长回文子串的左右端点下标:%d %d\n" , L , R);
            
            for(int i=L ; i<=R ; ++i) printf("%c" , s1[i] );
            printf("\n");
        }
    }
    return 0;
}

(由 manacher 模板题 HDU-3294 的代码改写)

上面在输出第一个最长回文子串的左右端点的下标时,遇到了由预处理后字符串下标转换得到原串中的下标的小烦恼。只需记住:预处理后字符串中,偶数位下标的字符都是 #,其下标与其右边的字符的下标整除2得到原串中对应字符的下标;另外,求得的最长回文子串的左右端必然是 #
在这里插入图片描述

参考资料

《Manacher算法的详细讲解》

https://www.jianshu.com/p/116aa58b7d81

感谢参考资料的作者!

上述资料与本文中的变量名的对应关系:

本文LCRp1p2p2L
上述资料cLCRp1p2pL

上述资料与本文中的分类讨论的对应关系:

本文abcd
上述资料第一种第二种(1)第二种(2)第二种(3)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值