扩展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]; } }
~ 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; }
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; }
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; }
计蒜客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; }
Nowcoder 1099C ($Distinct\ Substring$)
在字符串$s$的后面添加一个字符$c$后,会多产生$n+1$个以$c$为结尾的子串,那么就只需要考虑这些子串对于$h(c)$的贡献
考虑枚举题目中的$c$
那么剩下来的问题就是求最小的$i$使得$s[i...n]+c=s[L,R]$、且$s[R]=c$
这是因为,若$i'\geq i$,那么有$s[i'...n]+c=s[L+i'-i...R]$,即已被计入过了$f(s_1,...,s_n)$,不会对$h(c)$产生贡献;若$i'<i$,那么有$s[i'...n]+c\neq s[L+i'-i...R]$,会对$h(c)$产生$1$的贡献
要求$s[i...n]+c=s[L...R]$的最小$i$,可以将$s$串反转为$rs$,那么就变成了$c+rs[1...n-i+1]=rs[n-R+1...n-L+1]$;若不看$c$,就相当于$rs[1...n-i+1]=rs[n-R+2...n-L+1]$会对$h(rs[n-R+1])$产生贡献,显然用exkmp可以解决
最终,有$h(rs[n-R+1])=n+1-nxt[n-R+2]$
需要注意,若$c$已出现在$s$中过,那么需要对$h(c)$再减去$1$
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=1000005; const int MOD=1000000007; int nxt[N]; void getnxt(int *s,int m) { for(int i=0;i<m;i++) nxt[i]=0; 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]; } } int n,m; int s[N]; int vis[N],val[N]; int main() { while(scanf("%d%d",&n,&m)!=EOF) { for(int i=0;i<=max(n,m);i++) vis[i]=val[i]=0; for(int i=1;i<=n;i++) scanf("%d",&s[n-i]),vis[s[n-i]]=1; getnxt(s,n); for(int i=1;i<n;i++) val[s[i-1]]=max(val[s[i-1]],nxt[i]); ll ans=0,pw=3; for(int i=1;i<=m;i++) { ans^=(n+1-val[i]-vis[i])*pw%MOD; pw=pw*3%MOD; } printf("%lld\n",ans); } return 0; }
暂时先补到这里,遇到题目再放上来
(完)