扩展 KMP——Z 函数

本文建议进行适当缩小以适应 \(\KaTeX\) 行间公式。

本文符号规定与 \(\downarrow\) 文相同。

建议:前置知识(KMP)

\(\texttt{Upd 24.2.4}\) 修正(增删)描述。简洁多了。

引入

写本文的目的是让不了解扩展 KMP 的人快速(写完发现似乎无法那么快)理解。(作者在学扩展 KMP 时可谓焦头烂额,于是搞懂后便写下此文。)

文章较为复杂,有错请指出。

请一定静下心看完!理解了没有那么复杂!

扩展 KMP(Z 函数)

我们已经认识了前缀函数了。它是维护 一个字符串的所有前缀的 最长公共真前后缀的 长度——

\[\overbrace{{\color{red}s_0}\dots s_{\pi(i)-1}}^{\rightarrow}~s_{\pi(i)}\dots s_{i-\pi(i)}~\overbrace{s_{i-\pi(i)+1}\dots \color{red}s_i}^{\leftarrow}~s_{i+1}\dots\tag{1} \]

其中两段被括起来的子串相等。而 Z 函数——

\[\overbrace{{\color{red}s_0}\dots s_{z(i)-1}}^{\rightarrow}~s_{z(i)}\dots s_{i-1}~\overbrace{{\color{red}s_i}\dots s_{i+z(i)-1}}^{\rightarrow}~s_{i+z(i)}\dots\tag{2} \]

看出区别了吗?前缀函数是在 \([0,i]\) 区间向内“扩张”,Z 函数则是都向右“扩张”。图 \((2)\) 中被大括号括起的子串(一共两个但相等)叫匹配段(Z-box)。

形式化地说,\(z_i\) 是满足 \(s[0\dots x-1]=s[i\dots i+x-1]\)\(x\) 中最大的一个。注意它俩的长度均为 \(z_i\)


如何计算 Z 函数?

  1. 需要维护一个区间 \([l,r]\),满足它是当前已知的匹配段中 \(r\) 最大的一个。我们维护它的目的是利用它来加速暴力算法。(暴力算法指朴素地枚举 \(x\)。)在计算 \(z[i]\) 时我们保证 \(l\le i\)。初始时 \(l=r=0\)
  2. 然后枚举每一个 \(i\) 计算。

首先注意到既然 \([l,r]\) 是匹配段,必然满足 \(s[0\dots r-l]=s[l\dots r]\)(根据匹配段的定义):

\[\overbrace{s_0\dots s_{r-l}}~s_{r-l+1}\dots s_{l-1}~\overbrace{s_l\dots s_r}~s_{r+1}\dots \]

分类讨论:

\(\LARGE r<i\)

那么由于 \([l,r]\)\([i,n)\) 不相交,\([l,r]\) 就没有了利用价值,舍去,暴力计算 \(z_i\) 的值。

同时 \([l,r]\gets [i,i+z_i-1]\)

反之若 \(\LARGE i\le r\)

注意到 \(s[i-l\dots r-l]=s[i\dots r]\)。它们分别是 \(s[0\dots r-l],s[l\dots r]\) 的子串:

\[\overbrace{s_0\dots\underbrace{s_{i-l}\dots s_{r-l}}_\text{\color{red}these}}~s_{r-l+1}\dots s_{l-1}~\overbrace{s_l\dots\underbrace{s_i\dots s_r}_\text{\color{red}these}}~s_{r+1}\dots \]

即“\(\text{\color{red}these}\)”段一定相等。

如果 \(\Large z_{i-l}<r-i+1\)

即“\(\text{\color{red}these}\)”段比“\(\text{\color{blue}these}\)”段长,图示(无法全部显示可以浏览器缩小):

\[\small %\begin{aligned} %& \overbrace{\overbrace{{\color{red}s_0}\dots s_{z(i-l)-1}}^\text{\color{blue}these}~{\color{purple}s_{z(i-l)}}\dots\underbrace{\overbrace{s_{i-l}\dots s_{i-l+z(i-l)-1}}^\text{\color{blue}these}~{\color{lime}s_{i-l+z(i-l)}}\dots s_{r-l}}_\text{\color{red}these}}\dots%\\ %& \overbrace{s_l\dots\underbrace{\overbrace{{\color{red}s_i}\dots s_{i+z(i-l)-1}}^\text{\color{green}these}~{\color{lime}s_{i+z(i-l)}}\dots s_r}_\text{\color{red}these}}\dots %\end{aligned} \]

  1. 首先由于两个“\(\text{\color{blue}these}\)”段是 \(i-l\) 的匹配段,故相等。
  2. 其次由于两个“\(\text{\color{red}these}\)”段都是匹配段 \([l,r]\)(最大的黑色大括号)的子串,也相等。
  3. 再次由于“\(\text{\color{red}these}\)”段同时包含了“\(\text{\color{blue}these}\)”段、“\(\text{\color{green}these}\)”段,且位置、长度相同:
    可得“\(\text{\color{blue}these}\)”段 \(=\)\(\text{\color{green}these}\)”段。
  4. 显然 \({\color{lime}s_{i-l+z(i-l)}=s_{i+z(i-l)}}\)(都在“\(\text{\color{red}these}\)”段相同位置)。
  5. 由于 \(i-l\) 的匹配段仅为 “\(\text{\color{blue}these}\)”段,也即匹配分别在 \({\color{purple}s_{z(i-l)}},{\color{lime}s_{i-l+z(i-l)}}\) 处停止,可得它俩不相等(所以才停止匹配)。
  6. 有上 \(2\) 条可得 \({\color{purple}s_{z(i-l)}}\ne {\color{lime}s_{i+z(i-l)}}\)
  7. 观察发现上一条等价于 \(i\) 处匹配会在 \({\color{purple}s_{z(i-l)}},{\color{lime}s_{i+z(i-l)}}\) 处停止。\(z_i=z_{i-l}\)

总结:当 \(z_{i-l}<r-i+1\) 时:\(z_i=z_{i-l}\)

反之如果 \(\Large z_{i-l}\ge r-i+1\)

即“\(\text{\color{red}these}\)”段不比“\(\text{\color{blue}these}\)”段长,那么:

\[\small %\begin{aligned} %& \overbrace{\overbrace{\underbrace{{\color{red}s_0}\dots s_{r-i}}_\text{\color{orange}these}\dots s_{z(i-l)-1}}^\text{\color{blue}these}\dots\overbrace{\underbrace{s_{i-l}\dots s_{r-l}}_\text{\color{red}these}~\dots s_{i-l+z(i-l)-1}}^\text{\color{blue}these}}\dots%\\ %& \overbrace{s_l\dots\overbrace{\underbrace{{\color{red}s_i}\dots s_r}_\text{\color{red}these}~\dots s_{i+z(i-l)-1}}^\text{\color{green}these}}\dots %\end{aligned} \]

  1. 首先两个“\(\text{\color{red}these}\)”段相等、两个“\(\text{\color{blue}these}\)”段相等。
  2. 这回我们无法得到“\(\text{\color{blue}these}\)”段、“\(\text{\color{green}these}\)”段相等了,因为它们没有被“\(\text{\color{red}these}\)”段包含
  3. 但是,反之我们可以得到“\(\text{\color{orange}these}\)”段、“\(\text{\color{red}these}\)”段相等。它们都有被“\(\text{\color{blue}these}\)”段包含,且位置、长度相同。
  4. 那么“\(\text{\color{orange}these}\)”段、右边的“\(\text{\color{red}these}\)”段就组成了一个匹配段,所以至少 \(z_i=r-i+1\),不排除更长,所以要朴素更新

总结:当 \(z_{i-l}\ge r-i+1\) 时:\(z_i\gets r-i+1\),并需要朴素延长。

最终逻辑

  • \(r<i\) 时,\(z_i\gets0\),然后暴力尝试增加 \(z_i\)\((1)\)
  • 反之当 \(i\le r\) 时:
    • \(z_{i-l}<r-i+1\)\(z_i=z_{i-l}\)\((2)\)
    • 反之若 \(z_{i-l}\ge r-i+1\)\(z_i\gets r-i+1\),然后暴力尝试增加 \(z_i\)\((3)\)
    • 注意到 \((1),(3)\) 的暴力增加是可以合并的。

代码实现

题目:P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷

#include <iostream>
using namespace std;

const int N=20114514;
string a,b;
int n,m;
int z[N*2];

void calc_Z(string& s,int len)
{
	z[0]=len;
	for(int i=1,l=0,r=0;i<len;i++)
	{
		z[i]=0;
		if(i<=r&&z[i-l]<r-i+1)// (2)
		{
			z[i]=z[i-l];
		}
		else
		{
			z[i]=max(0,r-i+1);// (1),(3)
			while(i+z[i]<len&&s[z[i]]==s[i+z[i]])// 暴力增加
			{
				z[i]++;
			}
		}
		if(i+z[i]-1>r)// 试图更新 [l,r]
		{
			l=i;
			r=i+z[i]-1;
		}
	}
}
inline void exKMP()
{
	//         m  1  n
	string tmp=b+"#"+a;// 经典的拼接
	calc_Z(tmp,tmp.length());
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin>>a>>b;// a 中匹配 b
	n=a.length(),m=b.length();
	calc_Z(b,m);
	long long ans=0;// 不开 long long 见祖宗
	for(int i=0;i<m;i++)
	{
		ans^=(1ll*(i+1)*(z[i]+1));// 统计答案
	}
	printf("%lld\n",ans);
	exKMP();
	ans=0;
	for(int i=0;i<n;i++)
	{
		ans^=(1ll*(i+1)*(z[m+1+i]+1));// 统计答案
	}
	printf("%lld",ans);
	return 0;
}

后记

本文应该是笔者写的最认真的文章了,因为理解时很痛苦。

网络上也有很多其他优秀的介绍,也可以参考。

最后:画 \(\KaTeX\) 不易,点个赞吧!

posted @ 2023-11-15 20:19  Po7ed  阅读(70)  评论(0编辑  收藏  举报