扩展 KMP,Z 函数

引流前言

ACAM

Manacher

KMP

学完这些有助于你更好理解 Zfunc,甚至是一看就会!

1. 是什么

首先说说最长公共前缀。

不是感觉没什么好说的啊,复制下百科吧。

不是怎么百科搜不到啊。。。。

我们让 \(LCP(s,t)\) 表示字符串 \(s,t\) 的所有相同前缀中,最长的那一个。

LCP 就是 Longest Common Prefix 的 abbr.

扩展 KMP 算法,又称 Z 函数(Zfunc) 就是让你对于字符串 \(s\) 的每一个后缀 \(s_i\),求出 \(LCP(s,s_i)\)

我猜你会和我一样马上想到二分哈希。

xde :想到这个东西的人真是脑子里有包

你说得对二分哈希是 \(O(n\log n)\) 的,但是求 Zfunc 是可以做到 \(O(n)\) 的。

怎样球呢?

球球球。。

2. 怎么算

当然要尝试用之前算过的东西去更新现在要算的东西。

Manacher 和 KMP 点了个赞。

这里我们让 \(z\) 的下标从 \(0\) 开始。

考虑记录之前求解的 \(z_{i-1}\) 中,\(i+z_i-1\) 最大的那一个,即右端点最靠右的匹配段。我们记 \(l=\arg \max\{j+z_j-1\},r= \max \{j+z_j-1\}\),初始时 \(l=0,r=0\)

现在我们要求解 \(z_i\) 了。分情况:

  • 如果 \(i<=r\)

    那么 \(s[i,r]=s[i-l,r-l]\),这个东西就是 LCP 性质了,手摸一下不难得出。

    学这种东西不要懒得画画啊,勤奋画一画就懂了。

    现在考虑 \(z_{i-l}\) 会怎样贡献到 \(z_i\)

    • 如果 \(z[i-l] \le r-i+1\),也就是说如果直接继承 \(z[i-l]\) 不会超出 \(r\) 的右边界,因为 \(r\) 之后的东西关于 \(s\) 的 LCP 我们是未知的。因此我们可以直接继承,也就是:
      z[i]=z[i-l];

    • 如果 \(z[i-l] > r-i+1\),我们只能让它继承到 \(r-i+1\)。之后只能让它自己往后扩展了。因为 \(r\) 之后的东西关于 \(s\) 的 LCP 我们是未知的。

  • 如果 \(i>r\)
    那么再画个图你就发现这种情况怎么都无法贡献给 \(i\) ,于是只能让它自己往后扩展了。

此外,如果 \(r<i+z_i-1\),我们要更新 \(l,r\)

然后就有模板啦:

for(int i=1,l=0,r=0;i<len;i++){
  if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
  else{
    z[i]=max(0,r-i+1);// 这里把后两种情况合并到一起去了
    while(i+z[i]<len&&b[z[i]]==b[i+z[i]])z[i]++;
  }
  if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}

3. 怎么用

这东西应用还是挺好玩的。

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

如果你对一个模式串求出了它的 Zfunc,像 KMP 一样,你也可以求出任意一个主串对这个模式串的 Zfunc。

类似我们求一个 Zfunc 的过程,但是继承时不应继承正在求的这个串的 \(z\),因为这个串之前的 \(z\) 不能正确的贡献自己,只有匹配的模式串的 \(z\) 才可以。

上面这句话的意思是把 if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l]; 改成 if(i<=r&&z2[i-l]<r-i+1)z1[i]=z2[i-l];

另外,在进行 while 循环的暴力扩展时,注意 \(z\) 要同时小于两个串的长度,判断下即可。

这里还是画个图就能明白的事,一定要去画个图啊!

于是把上面的代码结合起来,你可以通过模板题了。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e7+5;
int z1[N],z2[N];
int main(){

  string a,b;cin>>a>>b;
  int len1=a.size(),len2=b.size();
  int len=b.size();
  z2[0]=len2;
  for(int i=1,l=0,r=0;i<len2;i++){
    if(i<=r&&z2[i-l]<r-i+1)z2[i]=z2[i-l];
    else{
      z2[i]=max(0,r-i+1);
      while(i+z2[i]<len&&b[z2[i]]==b[i+z2[i]])z2[i]++;
    }
    if(r<i+z2[i]-1)l=i,r=i+z2[i]-1;
  }
  int p1=0,p2=0;
  while(a[z1[0]]==b[z1[0]])z1[0]++;
  for(int i=1,l=0,r=0;i<len1;i++){
    if(i<=r&&z2[i-l]<r-i+1)z1[i]=z2[i-l];
    else{
      z1[i]=max(0,r-i+1);
      while(i+z1[i]<len1&&z1[i]<len2&&b[z1[i]]==a[i+z1[i]])z1[i]++;
    }
    if(r<i+z1[i]-1)l=i,r=i+z1[i]-1;
  }
  ll ans1=0,ans2=0;
  for(int i=0;i<len2;i++)ans1=ans1^(1ll*(i+1)*(z2[i]+1));
  for(int i=0;i<len1;i++)ans2=ans2^(1ll*(i+1)*(z1[i]+1));
  cout<<ans1<<'\n'<<ans2;
  
  return 0;
}

当然不止这一种做法,我们用字符串匹配题的一个常用技巧:把两个串拼起来。

本题中,我们让 \(b=b+a\),加号表示字符串拼接。

现在,我们直接求解拼接好的 \(b\) 的 Zfunc,于是你会发现:  

好像不对啊,由于把两个字符串拼起来算,因此两个数组不能越界,例如计算 \(b\)\(i\) 位置的 \(z_i\) 时,应满足 \(i+z_i < size(b)\)

当然我的第一个想法是在 for 循环里面加个越界限制:

for(int i=1,l=0,r=0;i<len;i++){
  if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
  else{
    z[i]=max(0,r-i+1); 
    while(i+z[i]<len&&b[z[i]]==b[i+z[i]])++z[i];
  }
  if(i<len2)z[i]=min(z[i],len2-i);// 这里
  if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}

但是这样你会 TLE #7,12。

但是其实我们不需要管这个越界,只需要在查询答案时取个最小值就好了。

为什么呢?

注意到越界本身对计算前缀这种东西没有影响(即使从 \(b_i\)
开始的 lcp 跑到了 \(a\) 的范围里去,它依然是和 \(b\) 的 lcp,可以参与对各 \(a_i\) 的贡献),因此我们只需在统计答案时取个最小值即可,不需要 for 循环里的那一句话。

下面是代码:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=4e7+5;
int z[N];
string a,b;
int main(){
  
  ios::sync_with_stdio(0);
  cin.tie();

  cin>>a>>b;
  int len1=a.size(),len2=b.size();
  b=b+a;
  z[0]=len2;
  int len=b.size();
  for(int i=1,l=0,r=0;i<len;++i){
    if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
    else{
      z[i]=max(0,r-i+1);
      while(i+z[i]<len&&b[z[i]]==b[i+z[i]])++z[i];
    }
    if(r<i+z[i]-1)l=i,r=i+z[i]-1;
  }
  z[0]=len2;
  ll ans1=0,ans2=0;
  for(int i=0;i<len2;i++)ans1=ans1^(1ll*(i+1)*(min(z[i],len2-i)+1));
  for(int i=len2;i<len;i++)ans2=ans2^(1ll*(i+1-len2)*(min(z[i],len2)+1));
  cout<<ans1<<'\n'<<ans2;
  
  
  return 0;
}

加入 IO 优化后,方法 1 要略快于方法 2。

P2375 [NOI2014] 动物园

这题有用 KMP Border 理论的 \(O(nL)\) 优秀算法,但是我还有一个用 Zfunc 的 \(O(nL)\) 算法。

首先对原串算出它的 Zfunc。

考虑每个 \(z_i\),你会发现既是它的后缀又是它的前缀还不能重叠这样的串在 \(i\) 位置就是 \(s[1,1]\)\(s[i,i]\)\(i+1\) 就是 \(s[1,2]\)\(s[i,i+1]\)。也就是这个东西会贡献给 \(i\) 及其之后的一些位置一个串的答案。

考虑这个位置是什么,如果 \(i\notin[1,z_i]\),那么显然 \([i,i+z_i-1]\) 的位置都是可以贡献到的,因为这些前缀后缀一定不会重叠。

但是如果 \(i\in[1,z_i]\),那么就只能贡献到 \([i,i+(i-1)-1]\) 的位置了,因为 \([1,i]\)\([i,i+z_i-1]\) 是有重合的,这样在此及其后的所有串都是不合法的,无法被贡献。

于是问题变成了一堆区间加和一次全局求和,这个东西差分数组就可以。时间复杂度是 \(O(nL)\)

下面的 code 用了树状数组,我也不知道我当时为什么要写树状数组。

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e6+6;
const int mod=1e9+7;
int nxt[N],z[N];
int c[N];
int m;
void add(int p,int k){for(;p<=m;p+=(p&(-p)))c[p]+=k;}  
ll pf(int r){
  ll sum=0;
  for(;r>0;r-=(r&(-r))){sum+=c[r];}
  return sum;
}
int main(){
  
  int n;cin>>n;
  while(n--){
    string s;cin>>s;
    m=s.size();
    memset(nxt,0,sizeof nxt);
    memset(z,0,sizeof z);
    memset(c,0,sizeof c);
  for(int i=1,l=0,r=0;i<m;i++){
    if(i<=r&&z[i-l]<r-i+1)z[i]=z[i-l];
    else{
      z[i]=max(0,r-i+1);
      while(i+z[i]<m&&s[z[i]]==s[i+z[i]])z[i]++;
    }
    if(r<i+z[i]-1)l=i,r=i+z[i]-1;
  }
    ll ans=1;
    for(int i=1;i<=m;i++){
      int lbc=min(i-1,z[i-1]);
      add(i,1);add(i+lbc,-1);
    }
    for(int i=1;i<=m;i++)ans=ans*(pf(i)+1)%mod;
    cout<<ans<<'\n';
  }
  
  return 0;
}

写完力!

有什么好题一会单独开个好题选讲。

posted @ 2025-06-05 21:27  hm2ns  阅读(26)  评论(0)    收藏  举报