Manacher算法小结

版权声明:本文为博主原创文章,未经博主允许也可以转载。 https://blog.csdn.net/qq_35541672/article/details/85253378


Manacher,又称马拉车算法,是解决字符串中回文的意大利器
本文主要是算法介绍,再加上几道例题

算法介绍

首先,为了不进行关于奇回文和偶回文的考虑,我们可以在原串中每个字母间加入一个特殊字符,比如#
例如babaa变成了#b#a#b#a#a#
我们惊奇地发现,原本的回文串bab变成了#b#a#b#,aa变成了#a#a#都是形式上的奇回文串了
然后就是算法流程了。

我们定义p[i]为以i为对称中心的最长的回文串的一半(上取整)
当前,我们最靠右的位置为mx,其所对应的回文中心为id
那么,比如我们当前处理到了i,就有这样几种情况
1. i<mx
我们考虑i关于id的对称点i’,那么,根据回文,有p[i]=min(p[i’],mx-i+1)
2.i>=mx
那么此时p[i]初值我们设为1
然后呢,我们就暴力扩展,最后再更新mx和id

时间复杂度?
考虑到如果暴力扩展每成功一次,mx就会升高一,那么暴力扩展次数就不会超过O(N)了
所以时间复杂度O(N)

代码参见例题1

例题

例题1 HDU 3068 最长回文

题意

多组数据,每次给出一个字符串S,求S的最长回文子串
|S|<=110000

题解

直接上Manacher

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=200000+5;
const int INF=0x3f3f3f3f;
char s[N];
char t[N];
int p[N];
int n,m;
void Get_t(char *s,char *t){
    n=strlen(s+1);
    m=n*2+1;
    t[0]=2;//!
    for(int i=1;i<=n;i++){
        t[i*2-1]=1;
        t[i*2]=s[i];
    }
    t[m]=1;
}
void Manacher(char *t){
    int mx=0,id=0;
    for(int i=1;i<=m;i++){
        if(mx>i)
            p[i]=min(p[id*2-i],mx-i+1);
        else
            p[i]=1;
        while(t[i+p[i]]==t[i-p[i]])
            p[i]++;
        if(p[i]+i-1>mx){
            mx=p[i]+i-1;
            id=i;
        }
    }
}
int main()
{
    while(~scanf("%s",s+1)){
        Get_t(s,t);
        //puts(t+1);
        Manacher(t);
        int ans=1;
        for(int i=1;i<=m;i++)
            ans=max(ans,p[i]-1);
        printf("%d\n",ans);
    }
}

例题2 bzoj2565 最长双回文串

题意

定义双回文串为一个串可以被分割成两部分,这两个部分都非空且为回文串
现给出一个串S,问你它的最长回文子串

题解

其实呢,我们只需要知道对于一个点而言,在它左右的最远的对称中心是谁,这两个坐标相减就是答案
至于这个怎么求?可以用两次单调队列(emm…这个单调队列是按位置单调…所以完全不用弹队尾)来实现。

成功打飚一个+让我以为时间复杂度分析错了…

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=400000+5;
const int INF=0x3f3f3f3f;
char s[N],t[N];
int p[N];
int n,m;
void Get_t(char *s,char *t){
    n=strlen(s+1);
    m=n*2+1;
    t[0]=2;
    for(int i=1;i<=n;i++){
        t[i*2-1]=1;
        t[i*2]=s[i];
    }
    t[m]=1;
}
void Manacher(char *t){
    int mx=0,id=0;
    for(int i=1;i<=m;i++){
        if(i<mx)
            p[i]=min(p[id*2-i],mx-i+1);
        else
            p[i]=1;
        while(i-p[i]>=1&&i+p[i]<=m&&t[i+p[i]]==t[i-p[i]])
            p[i]++;
        if(i+p[i]-1>mx){
            mx=i+p[i]-1;
            id=i;
        }
    }
}
int L[N],R[N];
int q[N],head,tail;
int main()
{
    while(~scanf("%s",s+1)){
        Get_t(s,t);
        //puts(t);
        Manacher(t);
        head=1,tail=0;
        for(int i=1;i<=m;i++){
            while(head<=tail&&q[head]+p[q[head]]-1<i)
                head++;
            q[++tail]=i;
            L[i]=q[head];
        }
        head=1,tail=0;
        for(int i=m;i>=1;i--){
            while(head<=tail&&q[head]-p[q[head]]+1>i)
                head++;
            q[++tail]=i;
            R[i]=q[head];
        }
        int ans=0;
        for(int i=2;i<m;i++)
            ans=max(ans,R[i]-L[i]);
        printf("%d\n",ans);
    }
}

例题3 Codeforces 17E Palisection

题意

给出一个字符串,问你有多少对回文子串有重叠(可以包含)

题解

考虑反面。
也就是说,我们先求出来总数,再减去不重叠的
具体怎么做呢?
首先我们要明确一件事,在我们加入了特殊字符后,回文串的两端都必须是这个特殊字符。也就是说[l,r]为回文串,是意味着[l+2,r-2]为回文串而非[l+1,r-1],不然的话在计算中就会出现很大的重复!
当然,还有一种做法就是干脆在马拉车之后就还原到原字符串上操作
我们定义L[i]为以i为左端点的回文串的数量,R[i]为右端点,g[i]为R[i]的前缀和
L[i]怎么求呢?我们可以差分。比如处理到以i为中心的回文子串时,让他的左端点+1,中间-1,就可以让他的左半部分数量都+1了(此处注意一下中间为#的话,中间的贡献也是不能算上的)。然后差分数组求个前缀和就是了
R[i]同理,只不过是差分数组的后缀和
现在想想,前缀和也没啥毛病
那么答案就是枚举每个(除端点)#隔开,左右两边独立,然后就用g[i]·L[i]就好了
注意不能用f[i]·g[i],否则也会有很大的重复
所以f其实没啥用…

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=(2e6+5)*2;
const int INF=0x3f3f3f3f;
const int mod=51123987;
char s[N],t[N];
int p[N];
int n,m;
int L[N],R[N];
int f[N],g[N];
void Get_t(char *s,char *t){
    n=strlen(s+1);
    m=n*2+1;
    t[0]=2;
    for(int i=1;i<=n;i++){
        t[i*2-1]=1;
        t[i*2]=s[i];
    }
    t[m]=1;
}
void Manacher(char *t){
    int mx=0,id=0;
    for(int i=1;i<=m;i++){
        if(i<mx)
            p[i]=min(p[id*2-i],mx-i+1);
        else
            p[i]=1;
        while(i-p[i]>=1&&i+p[i]<=m&&t[i+p[i]]==t[i-p[i]])
            p[i]++;
        if(i+p[i]-1>mx){
            mx=i+p[i]-1;
            id=i;
        }
    }
}
int main()
{
    scanf("%*d");
    while(~scanf("%s",s+1)){
        Get_t(s,t);
        //puts(t);
        Manacher(t);
        for(int i=2;i<m;i++){
            L[i-p[i]+1]++;
            if(i&1)
                L[i]--;
            else
                L[i+1]--;
        }
        for(int i=3;i<=m;i+=2)
            L[i]+=L[i-2];
        for(int i=m;i>=1;i--){
            f[i]=(f[i+1]+L[i])%mod;
        }
        for(int i=2;i<m;i++){
            R[i+p[i]-1]++;
            if(i&1)
                R[i]--;
            else
                R[i-1]--;
        }
        for(int i=m;i>=1;i-=2)
            R[i]+=R[i+2];
        for(int i=1;i<=m;i++){
            g[i]=(g[i-1]+R[i])%mod;
        }
        int ans=1ll*f[1]*(f[1]-1)/2%mod;
        for(int i=3;i<m;i+=2){
            ans=(ans-1ll*g[i]*L[i]%mod+mod)%mod;
        }
        printf("%d\n",ans);
    }
}

例题4 The Number of Palindromes

题意

多组数据。
给出一个字符串,问你有多少个本质不同的回文子串

题解

额…非常暴力
直接每次进while的时候,(注意,必须当前端点是特殊符号时,否则会重复)hash之后放进set即可
然而一不小心写了unsigned的我…WA到飞起…

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<string>
using namespace std;
typedef long long ll;
const int N=1000005*2;
const ll mod1=1e9+7;
const ll mod2=19260817;
const ll chr=131;
int n,m;
char s[N];
int t[N];
int p[N];
typedef pair<ll,ll> pll;
set<pll>S;
ll pw1[N],pw2[N];
ll s1[N],s2[N];
pll Get_hash(int l,int r){
    ll A=0,B=0;
    A=((s1[r]-s1[l-1]*pw1[r-l+1]%mod1)%mod1+mod1)%mod1;
    B=((s2[r]-s2[l-1]*pw2[r-l+1]%mod2)%mod2+mod2)%mod2;
    return pll(A,B);
}
void Manacher(){
    S.clear();
    int mx=0,id=0;
    for(int i=1;i<=m;i++){
        if(i<mx)
            p[i]=min(mx-i+1,p[id*2-i]);
        else
            p[i]=1;
        while(i-p[i]>=1&&t[i-p[i]]==t[i+p[i]]){
            p[i]++;
            if((i+p[i]-1)&1){
                //pll ZZZ=Get_hash(i-p[i]+1,i+p[i]-1);
                S.insert(Get_hash(i-p[i]+1,i+p[i]-1));
                //printf("%d\n",S.size());
            }
        }
        if(p[i]+i-1>mx){
            mx=p[i]+i-1;
            id=i;
        }
    }
}
int main()
{
    int Kase;
    scanf("%d",&Kase);
    pw1[0]=pw2[0]=1;
    for(int i=1;i<N;i++)
        pw1[i]=pw1[i-1]*chr%mod1,
        pw2[i]=pw2[i-1]*chr%mod2;
    for(int kase=1;kase<=Kase;kase++){
        memset(s,0,sizeof s);
        memset(t,0,sizeof t);
        memset(p,0,sizeof p);
        memset(s1,0,sizeof s1);
        memset(s2,0,sizeof s2);
        S.clear();
        scanf("%s",s+1);
        n=strlen(s+1);
        m=2*n+1;
        t[0]=28;
        for(int i=1;i<=n;i++)
            t[i*2-1]=27,t[i*2]=s[i]-'a'+1;
        t[m]=27;
        s1[0]=28,s2[0]=28;
        for(int i=1;i<=m;i++)
            s1[i]=(s1[i-1]*chr%mod1+t[i])%mod1,
            s2[i]=(s2[i-1]*chr%mod2+t[i])%mod2;
        Manacher();
        printf("Case #%d: %d\n",kase,S.size());
    }
}

例题5 codeforces 1080e Sonya and Matrix Beauty

题意

给出一个N*M的字母矩阵,定义一个子矩阵是hjb矩阵,当你把这个矩阵行上的字符任意排列(列不能动)后,可以使得到的矩阵横看竖看都是回文的
现在问你有多少个hjb矩阵
N,M<=250

题解

我们先枚举这个子矩阵的l和r
神来之笔啊喂
首先对于行的问题,只要字符出现次数是奇数不大于1个,这个行肯定是可以使其变成回文串的
然后我们考虑纵列,由于也是回文,所以肯定是对称的,那么如果要对称,这两行肯定各个字母数量都是相等的,而且如果数量相当,就一定能对称
这样看来,我们似乎可以直接干脆用各个字母数量来重新编号(注意要把那些不合法的改成其他的东西),就在纵列上做马拉车,求出纵列上的回文串数量,就是这个范围内的子矩阵数量
然后我就T了
主要是贪省事,直接扔进map重新编号…
然而我们最好是搞个cmp来比较
这个部分具体见代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
typedef long long ll;
const int N=505,M=26;
int n,m;
char s[N][N];
int sum[N][N][M];
int p[N*2];
bool ban[N];
int L,R;
bool cmp(int x,int y){
    if(x==0||y==0)
        return false;
    if(x&1&&y&1)
        return true;
    if(x&1||y&1)
        return false;
    x/=2,y/=2;
    if(ban[x]||ban[y])
        return false;
    for(int i=0;i<26;i++)
        if(sum[x][R][i]-sum[x][L-1][i]!=sum[y][R][i]-sum[y][L-1][i])
            return false;
    return true;
}
void Manacher(int len){
    int mx=0,id=0;
    for(int i=1;i<=len;i++){
        if(i<mx)
            p[i]=min(mx-i+1,p[id*2-i]);
        else
            p[i]=1;
        while(cmp(i-p[i],i+p[i]))
            p[i]++;
        if(p[i]+i-1>mx){
            mx=p[i]+i-1;
            id=i;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%s",s[i]+1);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            memcpy(sum[i][j],sum[i][j-1],sizeof sum[i][j]);
            sum[i][j][s[i][j]-'a']++;
        }
    int ans=0;
    for(int l=1;l<=m;l++)
        for(int r=l;r<=m;r++){
            L=l,R=r;
            memset(ban,0,sizeof ban);
            for(int k=1;k<=n;k++){
                int f=0;
                for(int z=0;z<26;z++){
                    if((sum[k][r][z]-sum[k][l-1][z])&1)
                        f++;
                }
                if(f>1)
                    ban[k]=1;
            }
            int len=2*n+1;
            Manacher(len);
            for(int i=2;i<len;i++)
                if(i&1||!ban[i/2]){
                    if(i&1)
                        ans+=(p[i]-1)/2;
                    else
                        ans+=p[i]/2;
                }
        }
    printf("%d\n",ans);
}

猜你喜欢

转载自blog.csdn.net/qq_35541672/article/details/85253378