扩展KMP(EXKMP)

时间:2020-04-11
本文章向大家介绍扩展KMP(EXKMP),主要包括扩展KMP(EXKMP)使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

 

跟Manacher类似,都是利用之前的计算结果来省去不必要的计算

感觉能搜到的博客写的都是同样的内容,而且并不是很直观...按照自己的理解写一遍

 

模板:

int nxt[N],ext[N];

void getnxt(char *s,int m)
{
    memset(nxt,0,sizeof(nxt));
    
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

void getext(char *s,int n,char *t,int m)
{
    memset(ext,0,sizeof(ext));
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}
View Code

 


 

~ KMP与EXKMP ~

 

回忆下KMP做了什么事情:

将待匹配串$S$与模式串$T$进行匹配,$|S|=n,|T|=m$;若$S[i+1]\neq T[j+1]$,那么不断使$j=next[j]$直到$S[i+1]$与$T[j+1]$能够匹配上(或者一个都匹配不上)

在这个过程中,我们能知道$S$与$T$是否能完全匹配

不过更严格地说,我们能够对于 从$S$每一个位置为结束的后缀 找到其在$T$中的最大匹配长度,即有

\[S[i-len+1\ ...\ i]=T[0\ ...\ len-1],\ i\in [0,n-1]\]

而EXKMP要做的事情跟KMP很类似,不过求的是 从$S$每一个位置开始的前缀 与$T$的最大匹配长度,即

\[S[i\ ...\ i+len-1]=T[0\ ...\ len-1],\ i\in [0,n-1]\]

 


 

~ 求nxt(next)数组 ~

 

我们先不考虑$S$数组,而是求$T$的每一个前缀与$T$的最大匹配长度$nxt[i]$,即有

\[T[i\ ...\ i+nxt[i]-1]=T[0\ ...\ nxt[i]-1],\ i\in [0,m-1]\]

假设$nxt[0\ ...\ i-1]$的结果已经计算完毕,现在将要计算$nxt[i]$

(显然$nxt[0]=m$;这是一个特例,不作为计算$nxt[1\ ...\ m-1]$的参考)

那么存在一个前缀起点$l\in [0,i-1]$,能够使得$l+nxt[l]-1$最大,将其记为$r=l+nxt[l]-1$;这代表了 以$T[1\ ...\ i-1]$为起点的前缀 最多匹配到的位置,那么有

\[T[l\ ...\ r]=T[0\ ...\ r-l]\]

考虑利用$T[l\ ...\ r]$来简化计算;将这个子串的起点从$l$变为$i$,由于$l\ ...\ r$均已匹配完毕,那么有

\[T[i\ ...\ r]=T[i-l\ ...\ r-l]\]

也就是说,以$i$开头的一段前缀与$i-l$开头的一段前缀相同;那么$nxt[i]$可以参考$nxt[i-l]$进行计算,但需要分情况讨论:

 

1. $i+nxt[i-l]-1<r$

左右两边同减$l$,得到$i-l+nxt[i-l]-1<r-l$,也就是说$T[i-l\ ...\ r-l]$仅能与$T[0\ ...\ nxt[i-l]-1]$完全匹配,之后的就匹配不上了

那么也就说明$T[i\ ...\ r]$最多能匹配到$T[0\ ...\ nxt[i-l]-1]$,即$nxt[i]=nxt[i-l]$

 

2. $i+nxt[i-l]-1\geq r$

仍然同减$l$,得到$i-l+nxt[i-l]-1\geq r-l$,也就是说$T[i-l\ ...\ r-l]$至少能与$T[0\ ...\ r-i]$完全匹配

也许以$i-l$开头的前缀能够与$T$匹配上更大的长度,但是这对于计算$nxt[i]$没有意义,因为我们仅有$T[i\ ...\ r]=T[i-l\ ...\ r-l]$,而$T[r]$之后的字符串我们并不知道

于是从$r+1$开始,暴力将$T[r+j]$与$T[r-i+j]$尝试进行匹配($j\geq 1,\ r+j<m$);直到$T[r+j']\neq T[r-i+j']$,则可以得到$nxt[i]=r-i+j'$

 

分析一下复杂度:

情况1显然$O(1)$就完事了;而情况2中的暴力匹配会使得$i+nxt[i]-1>r$,也就是将下一次计算的$r$不断右移,最多移动$m$次

于是整体为$O(m)$

 

在具体实现的时候,我们需要暴力计算一下$nxt[1]$以供之后的计算参考

void getnxt(char *s,int m)
{
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

 


 

~ 求ext(extend)数组 ~

 

上面所计算的$nxt$数组是为了计算$ext$数组服务的

而$ext$的计算跟$nxt$的计算方法几乎相同

假设$ext[0\ ... i-1]$已经计算完毕,且有$l\in [0,i-1]$使得$r=l+ext[l]-1$最大,那么有

\[S[l\ ...\ r]=T[0\ ...\ r-l]\]

将$S[l\ ...\ r]$的左端点移动到$i$,得到

\[S[i\ ...\ r]=T[i-l\ ...\ r-l]\]

于是$ext[i]$可以参考$nxt[i-l]$进行计算,注意这里是$nxt[i-l]$而不是$ext[i-l]$(因为是与$T[i-l\ ...\ r-l]$相同,而不是$S[i-l\ ...\ r-l]$)

之后的讨论略去了,是一样的

 

复杂度也是类似的与$r$的移动次数有关,为$O(n)$;于是总复杂度为$O(n+m)$

 

具体实现时需要暴力计算$ext[0]$

void getext(char *s,int n,char *t,int m)
{
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}

 


 

~ 一些题目 ~

 

Luogu P5410  (【模板】扩展 KMP(Z 函数))

$z$是对$nxt$求的,$p$是对$ext$求的;题目有点卡常

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=20000005;

int nxt[N],ext[N];

void getnxt(char *s,int m)
{
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

void getext(char *s,int n,char *t,int m)
{
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}

int n,m;
char s[N],t[N];

void read(char *S,int &len)
{
    char ch=getchar();
    while(ch<'a' || ch>'z')
        ch=getchar();
    while(ch>='a' && ch<='z')
        S[len++]=ch,ch=getchar();
}

int main()
{
    read(s,n),read(t,m);
    
    getext(s,n,t,m);
    
    long long ans;
    ans=0;
    for(int i=0;i<m;i++)
        ans^=1LL*(i+1)*(nxt[i]+1);
    printf("%lld\n",ans);
    ans=0;
    for(int i=0;i<n;i++)
        ans^=1LL*(i+1)*(ext[i]+1);
    printf("%lld\n",ans);
    
    return 0;
}
View Code

 

HDU 4333  ($Revolving\ Digits$)

考虑将字符串s复制一倍

那么若$nxt[i]\geq n$就说明产生循环(因为在这题中,出现重复数字仅可能出现在操作了循环节次时),由于题目要求数字互不相同可以直接break

否则比较$s[i+nxt[i]]$和$s[nxt[i]]$的大小即可

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int nxt[N],ext[N];

void getnxt(char *s,int m)
{
    memset(nxt,0,sizeof(nxt));
    
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

void getext(char *s,int n,char *t,int m)
{
    memset(ext,0,sizeof(ext));
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}

int n;
char s[N];

int main()
{
    int T;
    scanf("%d",&T);
    for(int kase=1;kase<=T;kase++)
    {
        scanf("%s",s);
        n=strlen(s);
        
        for(int i=n;i<n*2;i++)
            s[i]=s[i-n];
        n*=2;
        
        getnxt(s,n);
        
        int L=0,E=1,G=0;
        for(int i=1;i<n/2;i++)
        {
            if(nxt[i]>=n/2)
                break;
            else
                if(s[i+nxt[i]]<s[nxt[i]])
                    L++;
                else
                    G++;
        }
        printf("Case %d: %d %d %d\n",kase,L,E,G);
    }
    return 0;
}
View Code

 

HDU 3613  ($Best\ Reward$)

这是一道条件比较特殊的回文题目,用EXKMP跟Manacher差不多方便

由于题目要将一个字符串$s$切成两段,那么不妨看做左半边、右半边两个部分

将$s$左右翻转的串称为$rs$

对于右半边,如果它是回文串,那么相当于一个$s$的后缀能与$rs$匹配上,即$s[i...n-1]=rs[0...n-i-1]$

对于左半边,如果它是回文串,那么相反地,相当于一个$rs$的后缀能与$s$匹配上,即$rs[i...n-1]=s[0...n-i-1]$

每一半产生的贡献用前缀和处理一下就好了;最后扫一遍将两半拼起来、贡献相加

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=500005;

int nxt[N],ext[N];

void getnxt(char *s,int m)
{
    memset(nxt,0,sizeof(nxt));
    
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

void getext(char *s,int n,char *t,int m)
{
    memset(ext,0,sizeof(ext));
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}

int n;
char s[N],rs[N];
int val[30];

int pre[N];

inline int sum(int l,int r)
{
    return pre[r+1]-pre[l];
}

int L[N],R[N];

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        memset(L,0,sizeof(L));
        memset(R,0,sizeof(R));
        
        for(int i=0;i<26;i++)
            scanf("%d",&val[i]);
        
        scanf("%s",s);
        n=strlen(s);
        
        for(int i=0;i<n;i++)
        {
            rs[n-i-1]=s[i];
            pre[i+1]=pre[i]+val[s[i]-'a'];
        }
        
        getext(s,n,rs,n);
        
        for(int i=1;i<n;i++)
            if(i+ext[i]==n)
                R[i]=sum(i,n-1);
        
        getext(rs,n,s,n);
        
        for(int i=1;i<n;i++)
            if(i+ext[i]==n)
                L[n-1-i]=sum(0,n-1-i);
        
        int ans=-(1<<30);
        for(int i=0;i<n-1;i++)
            ans=max(ans,L[i]+R[i+1]);
        printf("%d\n",ans);
    }
    return 0;
}
View Code

 

计蒜客A2150  ($Mediocre\ String\ Problem$,$2018ICPC$南京)

第一次现场赛的题...心酸的回忆

题目要求找出$i,j,k$使得$s[i...j]+t[1...k]$;而其中特意规定了$j-i+1>k$,即有超过一半的长度在$s$中

那么显然$s[i...i+k-1]$与$t[1...k]$对称,而$s[i+k...j]$是回文

用EXKMP,我们可以求出$s$的以每个位置为结尾的后缀 与$t$的前缀 的公共对称长度$len$,即$s[i-len[i]+1...i]$与$t[1...len[i]]$对称;这可以转化成$s$的翻转串$rs$与$t$的公共前缀长度,即$rs[i...i+len[i]-1]=t[1...len[i]]$,用EXKMP跑一边就有了

之后就是计算以$s$的每一个位置为起点的回文串长度了;用Manacher跑一边后,对于以每个位置为中心的回文串进行差分(以该回文串前半段中任意一处作为起点都是可以的)得出所需的数组$par$

最后循环一遍,枚举$s$中对称部分的最右端$i$(即按照原题目意思中的$i+k-1$),那么每处的贡献就是$len[i]\times par[i+1]$

所以这题其实就是一个缝合怪,分析清楚了就不难

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N=1000005;

int p[2*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;
}

int nxt[N],ext[N];

void getnxt(char *s,int m)
{
    memset(nxt,0,sizeof(nxt));
    
    nxt[0]=m;
    for(int i=0;i<m-1 && s[i]==s[i+1];i++)
        nxt[1]=i+1;
    
    int l=1;
    for(int i=2;i<m;i++)
    {
        int r=l+nxt[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<m && s[i+j]==s[j])
                j++;
            nxt[i]=j;
            l=i;
        }
        else
            nxt[i]=nxt[i-l];
    }
}

void getext(char *s,int n,char *t,int m)
{
    memset(ext,0,sizeof(ext));
    getnxt(t,m);
    
    for(int i=0;i<min(n,m) && s[i]==t[i];i++)
        ext[0]=i+1;
    
    int l=0;
    for(int i=1;i<n;i++)
    {
        int r=l+ext[l]-1,j=i+nxt[i-l]-1;
        if(j>=r)
        {
            j=max(0,r-i+1);
            while(i+j<n && j<m && s[i+j]==t[j])
                j++;
            ext[i]=j;
            l=i;
        }
        else
            ext[i]=nxt[i-l];
    }
}

int n,m;
char s[N],t[N];
char ns[2*N],rs[N];

int par[N];

int main()
{
    scanf("%s",s);
    scanf("%s",t);
    n=strlen(s),m=strlen(t);
    
    for(int i=0;i<n;i++)
        rs[n-i-1]=s[i];
    
    ns[1]='#';
    for(int i=0;i<n;i++)
        ns[i*2+2]=s[i],ns[i*2+3]='#';
    
    manacher(ns,n*2+1);
    
    for(int i=1;i<=2*n+1;i++)
        if(i&1)
        {
            if(p[i]==1)
                continue;
            par[i/2-(p[i]-1)/2]++;
            par[i/2]--;
        }
        else
        {
            par[i/2-(p[i]-1)/2-1]++;
            par[i/2]--;
        }
    
    for(int i=1;i<n;i++)
        par[i]=par[i-1]+par[i];
    
    getext(rs,n,t,m);
    
    ll ans=0;
    for(int i=1;i<n;i++)
        ans+=1LL*ext[i]*par[n-i];
    printf("%lld\n",ans);
    return 0;
}
View Code

 


 

暂时先补到这里,遇到题目再放上来

 

(完)

$flag 上一页 下一页