zoukankan      html  css  js  c++  java
  • Manacher

    尽量找了个字符串里面最简单的部分来入门

    发现并没想象中的简单...

    模板:

    int p[N];
    
    //s,n均为插入过#的  返回最长回文串长度 
    int manacher(char *s,int n)
    {
        int mx=0,id=0,res=0;
        for(int i=1;i<=n;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            res=max(res,p[i]-1);
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        return res;
    }
    View Code

    Manacher能做的事情是,$O(n)$地求出以每个位置为中心的最长字符串长度;比$O(n^2)$的暴力不知道高到哪里去了

    不过,算法的步骤其实并不复杂

    首先,我们将一个字符串用占位符隔开

    比如:ababbabc,将其用“#”隔开,那么就是 #a#b#a#b#b#a#b#c#

    这样把字符串加倍有一个好处,就是可以表示长度为偶数的字符串中心

    比如#a#b#b#a#的中心就是从左到右第三个“#”

    接着,我们从$1$循环到$2n+1$(插入过占位符的字符串)依次求出$p[i]$

    $p[i]$的直接含义用文字表述出来有点麻烦,不如先观察一下上面的例子

    对于$i=4$,$s[i]=b,p[i]=4$,可以这样理解:

    以这个b为中心的最大回文串为 #a#b#a#,那么将b到两端的子串分别分离出来,即 #a#b 和 b#a#,他们的长度都是$4$,正是$p[i]$的大小

    于是,以$i$为中心的最大回文串 范围就是$[i-p[i]+1,i+p[i]-1]$;这个回文串的长度总是奇数(即$2 imes p[i]-1$)

    为了高效求出$p[i]$,我们需要维护两个值$mx,id$

    $mx$表示,对于所有$j<i$中 最大的$j+p[j]$,也可以理解为所有中心小于$i$的回文串中 最大的右端点位置 再$+1$;$id$表示的就是这个回文串中心$j$

    这两个值可以共同决定当前$p[i]$的下界,即代码中的关键部分

    for(int i=1;i<=n;i++)
    {
        p[i]=(mx>i?min(p[2*id-i],mx-i):1);
        ...
    }

    什么是$mx>i$?根据上面的约定,这表示$i$被以$id$为中心的回文串$P_{id}$所包含

    由于$i>id$,那么在$P_{id}$中,一定存在一个与$i$关于$id$对称的位置$2 imes id-i$;于是在$P_{id}$的范围中,以$i$为中心的子串就与 以$2 imes id-i$为中心的子串相同了,那么自然$p[i]$相同

    那么为什么是$min(p[2*id-i],mx-i)$呢?有可能以$2 imes id-i$为中心的最长回文子串 超出了$P_{id}$的范围,那么对应的,我们只能保证$i+p[i]leq mx$的部分是回文串,其余的只能暴力向后判断,故要将$p[2*id-i]$与$mx-i$取min

    至于$i=mx$的情况(不可能出现$i>mx$),由于不能参考之前的最长回文子串,于是暴力向后判断即可

    这样的算法为什么是$O(n)$的呢?我们需要考虑上面代码给$p[i]$赋的初值

    如果$p[i]=p[2*id-i]$,那么说明以$2 imes id-i$为中心的最长回文子串不超过$P_{id}$的范围,也就是说这个回文子串不可能再往外扩展了;于是之后的暴力判断会直接退出

    如果$p[i]=mx-i$或$p[i]=1$,那么说明$i+p[i]>=mx$,之后的暴力判断就必然会将$mx$的值增大;而$mx$最多只能增大$n$次,故一共只会进行$O(n)$级别的暴力判断


    一些题目,太难的还不太会...

    Luogu P3805  (【模板】manacher算法)

    原串中的最大回文子串长度 等于 最大的$p[i]-1$

    因为插入占位符后的新串中,最大回文子串的长度为$2 imes p[i]-1$,而这样的回文子串的两端一定是“#”;那么原串中对于的子串长度为$p[i]-1$

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N=25000010;
    
    int n,p[N];
    char s[N];
    
    int main()
    {
        scanf("%s",s+1);
        n=strlen(s+1);
        
        for(int i=2*n+1;i>=1;i--)
            s[i]=(i%2?'#':s[i/2]);
        n=2*n+1;
        
        int mx=0,id=0,ans=0;
        for(int i=1;i<=n;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            ans=max(ans,p[i]-1);
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    BZOJ 3790  (神奇项链)

    既然是多个回文串相拼,那么每个分别是最长回文子串是最优的;否则可以将多个串合并成一个拼上去,使得答案更优

    我们将以每个位置为中心的最长回文子串全部拿出来,那么就能得到字符串上的多个区间

    题目在这时就变成了最小区间覆盖,可以贪心解决

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N=100005;
    
    int n,p[N];
    char s[N];
    
    int to[N];
    
    int main()
    {
        while(~scanf("%s",s+1))
        {
            n=strlen(s+1);
            
            for(int i=2*n+1;i>=1;i--)
                s[i]=(i%2?'#':s[i/2]);
            n=2*n+1;
            
            int mx=0,id=0;
            for(int i=1;i<=n;i++)
            {
                p[i]=(mx>i?min(p[2*id-i],mx-i):1);
                while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]])
                    p[i]++;
                
                if(i+p[i]>mx)
                    mx=i+p[i],id=i;
            }
            
            memset(to,0,sizeof(to));
            for(int i=1;i<=n;i++)
            {
                int l=i-p[i]+1,r=i+p[i]-1;
                to[l]=max(to[l],r);
            }
            
            int ans=0,cur=0,rmost=0;
            for(int i=1;i<=n;i++)
            {
                rmost=max(rmost,to[i]);
                if(i>cur)
                    ans++,cur=rmost;
            }
            printf("%d
    ",ans-1);
        }
        return 0;
    }
    View Code

    Nowcoder 14943  (小G的项链)

    首先考虑$n$的约数数量应该不是很多,那么可以对每个约数分别判断

    对于某约数$k$,新项链长为$frac{n}{k}$

    我们要判断它是否能回文,就必须枚举一下起始位置;不过由于每$k$个项链合并一次,起始位置为$1$和为$k+1$是等价的,那么只需要枚举$k$个起始位置

    对于一个固定的起始位置,我们可以通过预处理前缀异或和,$O(frac{n}{k})$地快速获得新项链中的$frac{n}{k}$项

    对这个新数组复制一次(环上问题的常用套路)后做Manacher,看一看最长回文子串长度是否大于等于$frac{n}{k}$即可

    #include <cstdio>
    #include <locale>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N=800005;
    
    int n;
    int a[N],pre[N];
    
    int s[N],p[N];
    
    int manacher(int len)
    {
        int mx=0,id=0,res=0;
        for(int i=1;i<=len;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=len && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            res=max(res,p[i]-1);
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        return res;
    }
    
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]),a[n+i]=a[i];
        for(int i=1;i<=2*n;i++)
            pre[i]=pre[i-1]^a[i];
        
        int ans=0;
        for(int i=1;i<=n && !ans;i++)
        {
            if(n%i)
                continue;
            
            int m=4*n/i+1;
            for(int j=0;j<i;j++)
            {
                for(int k=1;k<=n/i;k++)
                    s[k]=pre[k*i+j]^pre[(k-1)*i+j],s[n/i+k]=s[k];
                for(int k=m;k>=1;k--)
                    s[k]=(k%2?0:s[k/2]);
                
                if(manacher(m)>=n/i)
                    ans=n/i;
            }
        }
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    Nowcoder 17062  (回文)

    考虑对每一个最长回文子串计算代价;若最中间不是最长回文子串,就会导致多删除元素

    那么接着就需要考虑中心回文串 两侧的两个子串应该如何处理

    由于它们两个在初始条件下不对称,那么至少需要完全删除一个子串,否则最内侧的两个元素不对称

    最优解有没有可能在两个子串中都先删除后添加呢?如果是这样的话,可以在左右两端各少添加一个元素,此时整个字符串仍为回文;故最优解的方案是,将一侧的子串全删完、将另一侧删一部分(可能不删),然后将删完的那一侧对称补全

    此时的总代价可以转化为,将一侧删完,将另一侧删一部分、再添加剩余的部分

    这就可以通过dp来解决了

    令$ldel[i],ladd[i]$分别表示 从左侧开始删到第$i$个元素、从右侧先删后添加到第$i$个元素的最小代价

    那么有$ldel[i]=ldel[i-1]+del[s[i]-'a'],ladd[i]=min(ldel[i],ladd[i-1]+add[s[i]-'a'])$;$rdel[i],radd[i]$同理

    于是对于原串中的每个极大回文子串$[l,r]$,取$min(ladd[l-1]+rdel[r+1],ldel[l-1]+radd[r+1])$就是此位置的代价

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    typedef long long ll;
    const int N=200005;
    
    int n,p[N];
    char s[N];
    
    int manacher(int len)
    {
        int mx=0,id=0,res=0;
        for(int i=1;i<=len;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=len && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            res=max(res,p[i]-1);
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        return res;
    }
    
    ll add[30],del[30];
    ll ladd[N],ldel[N],radd[N],rdel[N];
    
    int main()
    {
        scanf("%s",s+1);
        n=strlen(s+1);
        
        n=2*n+1;
        for(int i=n;i>=1;i--)
            s[i]=(i%2?'#':s[i/2]);
        
        manacher(n);
        
        for(int i=0;i<26;i++)
            scanf("%lld%lld",&del[i],&add[i]);
        
        for(int i=1;i<=n/2;i++)
            ldel[i]=ldel[i-1]+del[s[i*2]-'a'],
            ladd[i]=min(ladd[i-1]+add[s[i*2]-'a'],ldel[i]);
        for(int i=n/2;i>=1;i--)
            rdel[i]=rdel[i+1]+del[s[i*2]-'a'],
            radd[i]=min(radd[i+1]+add[s[i*2]-'a'],rdel[i]);
        
        ll ans=1LL<<60;
        for(int i=1;i<=n;i++)
        {
            int l=(i-p[i]+2)/2,r=(i+p[i]-1)/2;
            ans=min(ans,ldel[l-1]+radd[r+1]);
            ans=min(ans,ladd[l-1]+rdel[r+1]);
        }
        printf("%lld
    ",ans);
        return 0;
    }
    View Code

    BZOJ 2342  (双倍回文,$SHOI2011$)

    这个题目还是很有意思的,展示了$p[i]$数组的进阶用法

    对于一个双倍回文子串,我们可以(在添加了占位符的数组中)用$i$表示中点位置

    那么我们要找到最小的$j<i$满足$j+p[j]geq i$且$jgeq i-p[i]/2$

    其中,$j+p[j]geq i$防止了$i$为中心的回文子串$P_i$中间多出几个元素、导致仅为回文而不是双倍回文的情况

    而$jgeq i-p[i]/2$防止了$j$为中心的回文子串$P_j$总有一部分在$P_i$范围之外、导致不为双倍回文的情况

    这两个条件能够完全限制住符合条件的$j$(注意$i,j$都应为奇数,否则双倍回文串长度不为$4$的倍数),不过优先满足哪个条件会使得写法有一些不同

    如果优先满足$j+p[j]geq i$,那么可以将所有序号$j$按$j+s[j]$从大到小排序,接着从大到小循环$i$、将满足$j+s[j]geq i$的$j$全部压入一个set

    这时只需要在set中找到一个最小的$j$满足$jgeq i-p[i]/2$,用lower_bound就行了

    #include <set>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    typedef pair<int,int> pii;
    const int N=1000005;
    
    int n,p[N];
    char s[N];
    
    pii a[N];
    set<int> S;
    
    int main()
    {
        scanf("%d",&n);
        scanf("%s",s+1);
        
        for(int i=2*n+1;i>=1;i--)
            s[i]=(i%2?'#':s[i/2]);
        n=2*n+1;
        
        int mx=0,id=0;
        for(int i=1;i<=n;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        
        for(int i=1;i<=n;i+=2)
            a[i/2+1]=pii(i+p[i],i);
        sort(a+1,a+n/2+2);
        
        int ans=0,j=n/2+1;
        for(int i=n;i>=1;i-=2)
        {
            while(j>=1 && a[j].first>=i)
                S.insert(a[j].second),j--;
            
            int pos=i-p[i]/2;
            if(S.lower_bound(pos)!=S.end())
                ans=max(ans,(i-*S.lower_bound(pos))*2);
        }
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    如果优先满足$jgeq i-p[i]/2$,那么我们从小到大循环$i$,先找到最小的$j$满足$jgeq i-p[i]/2$,但是此时的$j$不一定满足$j+p[j]geq i$

    如果$j+p[j]<i$,那么就更加不可能$j+p[j]<i+1$了,故可以直接不再考虑$j$;这可以通过并查集中由$j$向$j+1$连边来实现

    这样在并查集中一直跳,直到第一个$j$满足$j+p[j]geq i$

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N=1000005;
    
    int n,p[N],fa[N];
    char s[N];
    
    int find(int a)
    {
        if(fa[a]==a)
            return a;
        return fa[a]=find(fa[a]);
    }
    
    int main()
    {
        scanf("%d",&n);
        scanf("%s",s+1);
        
        for(int i=2*n+1;i>=1;i--)
            s[i]=(i%2?'#':s[i/2]),fa[i]=i;
        n=2*n+1;
        
        int mx=0,id=0;
        for(int i=1;i<=n;i++)
        {
            p[i]=(mx>i?min(p[2*id-i],mx-i):1);
            while(i-p[i]>=1 && i+p[i]<=n && s[i-p[i]]==s[i+p[i]])
                p[i]++;
            
            if(i+p[i]>mx)
                mx=i+p[i],id=i;
        }
        
        int ans=0;
        for(int i=1;i<=n;i+=2)
        {
            int j=max(1,i-p[i]/2);
            if(j%2==0)
                j++;
            while(j+p[j]<i)
            {
                fa[j]=find(j+2);
                j+=2;
            }
            
            ans=max(ans,(i-j)*2);
        }
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    先咕咕咕,遇到题目再放上来

    (完)

  • 相关阅读:
    QR code 乱谈(一)
    用JAVA实现数字水印(可见)
    ctf总结
    Unix/Linux常用命令
    C语言概述
    C语言发发展历史
    为什么要学习C语言
    计算机应用领域
    计算机发展趋势
    如何学习计算机
  • 原文地址:https://www.cnblogs.com/LiuRunky/p/Manacher.html
Copyright © 2011-2022 走看看