• manacher(马拉车)算法详解+例题一道【bzoj3790】【神奇项链】


    这里写图片描述
    [pixiv] https://www.pixiv.net/member_illust.php?mode=medium&illust_id=39091399
    (CSDN好像有bug,不知道为什么存的草稿覆盖了之前的博客>.<,以后再也不存线上草稿了)

    昨天学了马拉车(manacher)算法,今天做了一道例题。虽然并不难,但还是写一写博客,即为民服务,又加深自己的理解。

    manacher是高效处理回文串的算法,不过因为只限求回文串,所以适用范围就窄了,但是它仍然有用,所以还是要学。

    首先是一个小小的处理技巧。因为回文串可能是奇数可能是偶数,偶数的时候就没有中心字符了,没有著脚点可恼火了。所以我们考虑用没有出现过的字符“#”来表示原串的间隔。eg。ababa->#a#b#a#b#a#。为了避免溢出,访问到不存在的点(’’=’’),我们再在首尾加上没有出现过的不同字符,eg’+’,’-‘。显然,现在对新串的每一个字符找以此字符为中心的字符串即可。

    manacher的思想就是从左到右求出以每个位置为中心的最长字符串(由于回文串很明显的包含和对称性质,以同一位置为中心的字符串自然是包含在最长回文串里),利用回文串的对称性质,对每一个位置利用之前的信息来快速得到答案。近似于扫一遍,所以均摊o(n)(之后详细证明)

    那么之前的信息是什么呢?我们记向右延伸最远的回文串(有多个的话,选中心点最靠左的),令其右端点为mx,中心点的下标为id。我们需要处理数组pal[i],表示以i为中心的回文串的右端到i的长度。

    下面就是怎么实现的问题了。
    对于一个位置i,有三种情况
    1、i<=mx,我们可以由对称性得出其关于id的对称点的pal[]
    这里写图片描述
    此时易得pal[i]=pal[2*id-i];
    2、同样的情况,i<=mx,但是其对应点的范围超出了id的范围。
    这里写图片描述
    此时可得pal[i]>=mx-i+1;
    由对称性可得,在id范围内的部分i可由2*id-i推来,但外面的部分必定与另一边不同(想想),暴力扩充即可,将mx更新。暴力判断pal[i]能到多少,每次判断成功都等价于mx++,失败则结束。由于mx是单增的,于是判断成功的次数不超过串长次,均摊o(n)
    3、i>mx。处理方法与2相同。

    放个代码更好理解

    void manacher(){
        int mx=0,id;
        for(int i=1;i<=m;i++){
            if(mx>=i) pal[i]=min(mx-i+1,pal[2*id-i]);
            else pal[i]=1;
            while(s[i-pal[i]]==s[i+pal[i]]) pal[i]++;
            if(i+pal[[i]-1>mx)
                mx=i+pal[i]-1,id=i;
            }
        }
    }

    由代码可以也可以看出,mx是单增的,均摊o(n)

    此外,manacher算法还引出了一个——

    极重要的性质

    这里写图片描述
    再来看这张图,我们发现,如果mx不更新,就不会出现本质不同的回文子串,因为前面已经出现过了;而每扩展一次mx,最多新出现一个本质不同的回文子串。
    于是得到性质:#一个字符串最多只有n个本质不同的回文子串#。这个性质很重要,有些题会用到,需要这个性质去分析。

    基础讲完了,终于可以放题了:D

    Description
    母亲节就要到了,小 H 准备送给她一个特殊的项链。这个项链可以看作一个用小写字
    母组成的字符串,每个小写字母表示一种颜色。为了制作这个项链,小 H 购买了两个机器。第一个机器可以生成所有形式的回文串,第二个机器可以把两个回文串连接起来,而且第二个机器还有一个特殊的性质:假如一个字符串的后缀和一个字符串的前缀是完全相同的,那么可以将这个重复部分重叠。例如:aba和aca连接起来,可以生成串abaaca或 abaca。现在给出目标项链的样式,询问你需要使用第二个机器多少次才能生成这个特殊的项链。
    Input
    输入数据有多行,每行一个字符串,表示目标项链的样式。
    Output
    多行,每行一个答案表示最少需要使用第二个机器的次数。
    Sample Input
    abcdcba
    abacada
    abcdef
    Sample Output
    0
    2
    5
    HINT
    每个测试数据,输入不超过 5行
    每行的字符串长度小于等于 50000

    先用manacher跑出pal[ ]数组,因为题目说可以重合,于是就转换成了经典的区间覆盖问题,贪心即可。

    代码

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    
    const int N=50000+5;
    
    int len,pal[N*2];
    char s[N],a[N*2];
    struct Node{
        int le,ri;
    }qu[N*2];
    
    bool cmp(Node a,Node b){
        return a.le<b.le;
    }
    void insert(){
        memset(a,0,sizeof(a));
        int lena=-1;
        a[++lena]='+';
        for(int i=0;i<len;i++){
            a[++lena]='#';
            a[++lena]=s[i];
        }
        a[++lena]='#';
        a[++lena]='-';
        len=lena-1;
    }
    void manacher(){
        int mx=0,id;
        for(int i=1;i<=len;i++){
            if(mx>=i) pal[i]=min(mx-i+1,pal[2*id-i]);
            else pal[i]=1;
            while(a[i-pal[i]]==a[i+pal[i]]) ++pal[i];
            if(mx<i+pal[i]-1)
                mx=i+pal[i]-1,id=i;
        }
    }
    int fugai(){
        int ans=0,far=1;
        int i=1;
        for(i=1;qu[i].le<=1;i++)
            if(qu[i].ri>qu[far].ri) far=i;
        while(i<=len){
            ans++;
            int tmp=far;
            for(;qu[i].le<=qu[far].ri;i++)
                if(qu[i].ri>qu[tmp].ri) tmp=i; 
            far=tmp;
        }
        return ans;
    }
    int main(){
        while(scanf("%s",s)!=EOF){
            len=strlen(s);
            insert();
            manacher();
            memset(qu,0,sizeof(qu));
            for(int i=1;i<=len;i++)
                qu[i].le=i-pal[i]+1,qu[i].ri=i+pal[i]-1;
            sort(qu+1,qu+len+1,cmp);
            printf("%d
    ",fugai()-1);
        }
        return 0;
    }
  • 相关阅读:
    螺旋矩阵算法
    shell脚本编程的10个最佳实践
    时间字符串转长整形数
    python的发音
    wget使用技巧
    History命令用法15例
    14位格式时间字符串
    Spring MVC
    Android SQLite数据储存方式
    MYSQL命令大全
  • 原文地址:https://www.cnblogs.com/LinnBlanc/p/7763149.html
Copyright © 2020-2023  润新知