跳到主要内容

Z 函数

概述

Z 函数记录字符串与各个后缀的最长公共前缀长度,国内常把计算它的算法称为扩展 KMP。算法维护当前右端最靠右的匹配段;若待求位置落在段内,就先复用对应位置的信息,再按需继续向右扩展;否则从头扩展。由于每次扩展都会推动右端点,总复杂度为 O(n)。应用包括用分隔符拼接模式串与文本后进行匹配,以及判断字符串的周期与最小循环节。

参考资料​

简介​

对于长度为 nn 的字符串 ss,定义 Z 函数(Z-function)ziz_i 为 ss 与 s[i…n−1]s[i\dots n-1] 的最长公共前缀(LCP)的长度,特别地 z0=0z_0=0。国内通常称计算该数组的算法为 扩展 KMP(exKMP)。

维护当前右端点最靠右的匹配段 [l,r][l,r],则 s[l…r]s[l\dots r] 是 ss 的前缀。计算 ziz_i 时:若 i≤ri\le r,则 zi≥min⁡(zi−l,r−i+1)z_i\ge\min(z_{i-l},r-i+1);否则从头暴力扩展。每次扩展都使 rr 至少右移 1,总时间复杂度为 O(n)O(n)。

实现​

436 Bcpp
#include <bits/stdc++.h>
using namespace std;

const int N=500005;
int z[N];
void z_func(string s)
{
int n=s.size();
z[0]=0;
for(int i=1,l=0,r=0;i<n;i++)
{
z[i]=i<=r?min(z[i-l],r-i+1):0;
while(i+z[i]<n&&s[z[i]]==s[i+z[i]])z[i]++;
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin>>s;
z_func(s);
int n=s.size();
for(int i=0;i<n;i++)cout<<z[i]<<' ';
return 0;
}

应用​

模式匹配:求模式串 pp 在文本 tt 中的所有出现位置。取分隔符 #(不在两串中),对 p+#+tp+\#+t 求 Z 函数,凡 zi=∣p∣z_i=|p| 的位置都对应 tt 中的一次匹配。

最小循环节:枚举 ii,若 i+zi=ni+z_i=n,则 ss 的前 ii 个字符构成一个周期;最小的这样的 ii 即最小循环节长度。若还满足 n mod i=0n\bmod i=0,则 ss 由该循环节整数次重复而成。

例题​

给定两个字符串 a,ba,b,你要求出两个数组:

  • bb 的 zz 函数数组 zz,即 bb 与 bb 的每一个后缀的 LCP 长度。
  • bb 与 aa 的每一个后缀的 LCP 长度数组 pp。

对于一个长度为 nn 的数组 aa,设其权值为 xor⁡i=1ni×(ai+1)\operatorname{xor}_{i=1}^n i\times(a_i+1)。