码迷,mamicode.com
首页 > 其他好文 > 详细

Manacher

时间:2020-02-26 01:25:39      阅读:18      评论:0      收藏:0      [点我收藏+]

标签:级别   ref   预处理   bbb   模板   insert   需要   转化   img   

 

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

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

 

模板:

技术图片
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;
}
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\times 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\times id-i$;于是在$P_{id}$的范围中,以$i$为中心的子串就与 以$2\times id-i$为中心的子串相同了,那么自然$p[i]$相同

那么为什么是$min(p[2*id-i],mx-i)$呢?有可能以$2\times 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\times 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\times 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\n",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\n",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\n",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\n",ans);
    return 0;
}
View Code

 

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

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

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

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

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

而$j\geq 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$满足$j\geq 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\n",ans);
    return 0;
}
View Code

如果优先满足$j\geq i-p[i]/2$,那么我们从小到大循环$i$,先找到最小的$j$满足$j\geq i-p[i]$,但是此时的$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\n",ans);
    return 0;
}
View Code

 


 

慢慢补一点更难的题

(待续)

Manacher

标签:级别   ref   预处理   bbb   模板   insert   需要   转化   img   

原文地址:https://www.cnblogs.com/LiuRunky/p/Manacher.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有 京ICP备13008772号-2
迷上了代码!