Z函数&前缀函数的总结~

这篇总结全部的字符串都是以 0 为下标起点

Z函数(ExKMP)

对于一个字符串 \(S\)c++

咱们规定一个函数 \(Z[i]\) 表示 \(S\)\(S[i...n-1]\) 的 LCP(最长公共前缀)的长度。算法

\(S[0.....Z[i]-1]\)\(S[i...i+Z[i]-1]\) 相等数组

先说构造 \(Z\) 函数,再说 \(Z\) 函数的应用函数

首先考虑暴力的构造 时间复杂度 \(O(n^2)\)优化

char s[N];
inline void GetZ(){
    int len=strlen(s);
    for(register int i=0;i<len;++i)
        while(i+z[i]-1<n&&s[z[i]]==s[i+z[i]]) z[i]++;
    
    for(register int i=0;i<len;++i)
        cout<<z[i]<<" ";
}

这就时一个根据定义的模拟,可是显然 \(O(n^2)\) 的时间复杂度有些不太优秀,因此考虑优化:spa

扩展时的判断条件根据上面的代码,应该是:code

while(i+z[i]-1<n&&s[z[i]]==s[i+z[i]]) z[i]++;

这一步是用枚举实现的,是 \(O(n)\) 的,那么如何对这一步进行优化呢?blog

对于枚举的优化:

这时考虑先考虑一下 \(Z\) 函数的性质:继承

从定义来讲:这是知足$Z[0.....Z[i]-1] $与 \(Z[i,i+Z[i]-1]\) 相等的最长长度ip

性质1:那么对于一个区间\([l,r]\)\(l \in [i,i+Z[i]-1]\)\(r\in [i+Z[i]-1]\),它必定与区间 \([l-i,r-i]\) 相等(定义),

那么考虑优化暴力的思路,即如何减小枚举:

如何减小枚举呢?大部分状况来讲是从当前已知的状况去更新当前未知的状况,若是不行,再枚举

记录下\(i+Z[i]-1\) 的最大值 \(r\) ,与这个最大值对应的 \(i\),下面出现的 \(l\),就是这个最大值对应的 \(i\)

若是对于当前的一个位置 $i $,若是 \(i \leq r\)。那么根据性质 \(1\) , \(S[i....r]\) 是与 \(S[i-l.....r-l]\) 相等的

因此要么 \(i\) 这个位置与 \(Z[i-l]\) 同样,与 \(S\) 的LCP长度为 \(Z[i-l]\),要么它能够匹配完整个 \(r-i+1\),还能够继续日后匹配。

简单来讲,就是\(Z[i] \geq min(r-i+1,Z[i-l])\)

那么若是此时 \(Z[i-l]\) 还知足 \(Z[i-l] < r-i+1\) 也就是当前能够继承的范围并无到达此时的边界 \(r\) ,咱们选择直接继承。

if(Z[i-l]<r-i+1) Z[i]=Z[i-l];

根据上面的分析,若是不知足上面的这个条件话,证实它能够匹配完整个 \(r-i+1\),而且还能向后匹配

因此代码也就更简单了:

if(Z[i-l]>=r-i+1){
	Z[i]=r-i+1;
	while(i+Z[i]-1<len&&S[Z[i]]==S[i+Z[i]])	Z[i]++;
}

可是咱们发现上面的两个程序自己是没有问题的,只是有一些状况没有考虑到:

1.好比当前的位置 \(i\),若是已经 \(>r\) 了,那么上面的全部结论都不成立。这时就应该直接暴力匹配
2.咱们的 \(r\),表示的是当前匹配段最右边的端点值,而 \(l\) 是它所对应的 \(i\) 值,因此在暴力匹配后,应该更新 \(l,r\) 的值。

因此整个求 \(Z\) 函数的代码应该是这样的:

int len=strlen(s);
	Z[0]=0;//其实根据定义这里也珂以赋值为 len。
	for(register int i=1;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);
//由于可能有两种状况进来,一个是i>r,一个是Z[i-l]>=r-i+1,而两种状况对于Z[i]的赋值是不一样的。因此这里直接一个max(0,r-i+1)归纳两种状况
			while(i+Z[i]-1<len&&S[Z[i]]==S[i+Z[i]])	Z[i]++;
			if(r>i+Z[i]-1)	l=r,r=i+Z[i]-1;
		}
	}

为何咱们的循环要从1开始呢?

由于若是从0开始的话,\(r\) 会直接扩展完,而整个算法也会随之退化到 \(O(n^2)\)

Z函数的应用:

1.字符串匹配

一个字符串算法少不了的就是字符串匹配了。

一道经典例题:

求一个字符串 \(A\) ,在另外一个字符串 \(B\) 中的出现次数。

你先想了想 \(Z\) 函数,发现它储存的都是 \(B\)的后缀与 \(B\) 匹配的信息,基本没法应用到与 \(A\) 匹配上面。

那么如何将 \(B\)\(B\) 匹配的信息变成 \(B\)\(A\) 统计的信息呢?

答案十分 \(Naive\)

\(A\) 加在 \(B\) 的前面不久好了?

此时在新的字符串中 \(A\) 是这个串的前缀,那么此时匹配的就都是 \(A\) 了。

固然这样是有问题的,好比位置 \(i\) 的后缀已经能够把 \(A\) 所有匹配完了,他仍是会和本身匹配,那么此时的信息根本没法用到与 \(A\) 的匹配中去。

因此咱们还须要在 \(A\)\(B\) 之间加上一个特殊符号 '#',从而保证匹配长度不会超过 \(len_A\)

那么统计出现次数时只须要统计在 \(B\) 串的范围内,有多少个位置知足\(Z[i]=len_A\) 的就好了。

有了上面字符串匹配的知识,你就能够 \(A\)掉一些简单的模板题了!

题目:
P5410 【模板】扩展 KMP(Z 函数)
CF126B Password
UVA12604 Caesar Cipher

2.判断循环节

几个概念:

对字符串 \(S\)\(0> p \leq |S|\),若 \(S[i]=S[i+p]\) 对全部 \(i \in [0,|S|-p-1]\) 成立,则称 \(p\)\(S\)周期

对字符串 \(S\)\(0 \leq r <|S|\),若 \(S\) 长度为 \(r\) 的前缀 和 长度为 \(r\) 的后缀相等,则称长度为$ r$ 的前缀为 \(S\) 的 $ border$。

注意,周期不等价于循环节!

若是一个长度为 \(k\) 的周期是循环节,那么必定知足 \(len\% k=0\)

题目

求一个字符串 \(A\) 的最短循环节。

对于一个长度为 \(k\) 的循环节,必定知足\(S[0......k-1]=S[len-k.....len-1]\)

若是转化为 \(Z\) 函数的话,就是 \(i+Z[i]==len\) 就是 \(i\) 的后缀为 \(S\) 的一个Border,有一个长度为 \(Z[i]\)\(border\) 等价于有一个长度为 \(len-Z[i]\) 的周期。(证实略过)

那么咱们能够 \(O(n)\) 的扫,若是当前\(i+Z[i]==len\) 那么判断 \(len\%(len-Z[i])\) 是否等于 \(0\) 。由于知足 \(i+Z[i]=len\)\(len-Z[i]\) 是递减的(由于 \(i\) 枚举时递增。)因此第一个知足上述条件的 \(len-Z[i]\) 就是最大的循环节,要找最小的能够直接倒叙枚举,而后第一个直接退出。

例题:
UVA455 周期串 Periodic Strings

(由于我太弱了,因此我没有找到更多的循环节例题 )

3.判断回文

只要你理解了 \(Z\) 函数在字符串匹配的应用。若是要判断一个串 \(S\) 是否为回文,只须要将它的反串 \(S'\) 拼在 \(S\) 前面,而后中间加上一个 '#' ,直接匹配,最后判断 \(Z[0]\) 是否等于 \(len\) 就行了。

例题:
UVA11475 Extend to Palindrome

题意:

就是加最少的字母,使得原串变为一个回文串。

设当前的字符串为 \(S\)\(S\) 必定能够被分红两部分 \(A\)\(B\)

其中\(B\)是一个回文串(也能够是一个空串),\(A\) 是一个普通的字符串。

放一个图方便理解吧:

\(A\) 的反串为 $ A'$

并且 \(A+B+A'\) 必定是一个回文串(想想为何)

那么咱们加上的字符串就是 \(A'\)

由于\(|A'|\) = \(|A|\),\(|A|=|S|-|B|\)

由于\(|S|\)必定,为了让\(|A'|\)更小,因此须要找到最大的\(|B|\)

也就是找出 \(S\) 的后缀中最长的回文串。

这个利用 \(Z\) 函数很容易解决

咱们将 \(S\) 的反串 \(S'\) 拼在 \(S\) 的前面,那么一个后缀回文串左端点 \(i\) 必定知足 \(Z[i]=\)这个后缀回文串的 \(len\) ,也就是\(i+Z[i]=\) 整个字符串的 \(len\),即\(i+Z[i]=len_S\)

记住,咱们找的是最长的后缀回文串,也就是 \(|B|_{max}\)

但答案须要的是\(|A|\),而且还要将 \(S[0\)~\(|A|\)-\(1]\)倒过来输出

最后输出就能够了。

Code:

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+3;
char s[N];
int len,z[N],siz;
inline void GetS(){
	z[0]=siz+1;
	for(int i=1,l=0,r=0;i<=siz;++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]<=siz&&s[z[i]]==s[i+z[i]]) ++z[i];
			if(i+z[i]-1>r)	l=i,r=i+z[i]-1;	
		}
	}
	return;
}
int main(){
	while(scanf("%s",s)!=EOF){
		len=strlen(s);siz=2*len;
		s[len]='#';
		for(register int i=len+1;i<=siz;++i)	s[i]=s[i-len-1];
		reverse(s,s+len);
		GetS();int maxn=0;
		for(register int i=siz;i>len;--i){if(z[i]==siz-i+1){maxn=z[i];}	}	
		maxn=len-maxn;
		for(register int i=len+1;i<=siz;++i)	cout<<s[i];
		reverse(s+len+1,s+len+1+maxn);
		for(register int i=len+1;i<=len+maxn;++i)	cout<<s[i];
		putchar('\n');
	}
	return 0;
}

4.完美子串?

对于一个串 \(S\),若是一个串既是它的前缀又是它的后缀,那么他就是 \(S\) 的完美子串。用 \(Z\) 函数来讲,就是 \(i\) 若是知足 \(i+Z[i]==len\)\(i\) 开头的后缀为完美子串。

一些变式

1.求完美子串的出现次数:

首先注意到,每个完美子串的长度都不相同,这就意味这咱们不须要判断一个完美子串与另外一个完美子串是否本质相同。

并且大的完美子串中必定包含小的完美子串,这也就启发咱们能够利用 桶+后缀和 的思想来统计出现次数。

那么如何判断某一个子串能够包含某一个大的完美子串( \(k\) )呢?很显然,只须要这个点 \(i\)\(Z[i]\geq len_k\) 就好了(由于每个完美子串也是一个前缀。)

例题:
CF126B Password
CF432D Prefixes and Suffixes


//\(Z\) 函数蒟蒻会的就这么点了。。。以为好的点个赞呗~(赞在文章底部做者栏的右边)

前缀函数

好吧其实前缀函数和 \(KMP\)\(next\) 数组没什么大区别,只不过一个是下标一个是长度罢了。

给定一个长度为 \(len\) 的字符串 \(S\) , 其前缀函数被定义为一个长度为 \(n\) 的数组 \(\pi\)。其中\(\pi[i]\) 的定义为:

1.若是 \(i\) 的前缀 \(S[0...i]\) 有一对相等的真前缀与真后缀,即 \(S[0.....k-1]=S[i-k+1.....i]\) 那么 \(\pi[i]\) 就是这个相等的真前缀的长度,也就是 \(\pi[i]=k\)

2.若是有不止一对相等的,那么 \(\pi[i]\) 就是其中最长的那一对的长度;

3.若是没有相等的,那么 \(\pi[i]=0\)

简单来讲 \(\pi[i]\) 表示的也就是以 \(i\) 为右端点的前缀最长的 \(border\) 长度( \(border\) 的定义看上面)

特别的,咱们规定 \(\pi[0]=0\)

若是直接暴力计算前缀函数的话:

Code:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	for(register int i=1;i<len;++i){
		for(register int j=i;j>=0;--j){
			if(s.substr(0,j)==s.substr(i-j+1,j)){
				pi[i]=j;
				break;
			}
		}
	}
	return;
}

显然上面的算法是 \(O(n^3)\) 的,不够优秀

考虑优化

优化构造前缀函数

优化1:相邻的两个前缀函数值最多增长 1。

这个显然,若是已经求出了当前的 \(\pi[i]\) 须要求出一个尽可能大的 \(\pi[i+1]\) 时。

\(S[i+1]=S[\pi[i]]\) 的(下标从 \(0\) 开始),此时的 \(\pi[i+1]=pi[i]+1;\)

因此从 \(i\)\(i+1\) 时,前缀函数值只可能增长 \(1\), 或者维持不变,或者减小。

此时能够将整个代码优化成这样:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	for(register int i=1;i<n;++i){
		for(register int j=pi[i-1]+1;j>=0;--j){
			if(s.substr(0,j)==s.substr(i-j+1)){
				pi[i]=j;
				break;
			}	
		}
	}
	return;
}

这个时候,由于起始点变为了 \(\pi[i-1]+1\) 因此只有在最好的状况下才会在这个枚举上限上 \(+1\) ,因此最多的状况时会进行 \(n-1+n-2+2n-3\) 次比较

因此这个时候整个算法时间复杂度已是 \(O(n^2)\) 了。但仍是不够优秀

优化2:能够经过不断地跳前缀函数来获取一个合法的匹配长度

在优化1中,我讨论了最优状况下的转移,那么这时理所固然的就该来优化\(S[\pi[i]]!=S[i+1]\) 时的匹配了

咱们在 \(S[\pi[i]]!=S[i+1]\) 时,根据 \(\pi\) 函数的最优性,咱们应该找到第二长的长度 \(j\) 使得 \(S[0....j-1]==S[i-j+1.....i]\) 这样咱们才能继续用 \(S[i+1]=S[j]\) 时的拓展。

而当咱们观察了一下能够发现:

\(S[0.....\pi[i]-1]=S[i-\pi[i]+1....i]\) 因此第二长 \(j\) ,也就等价于\([0,\pi[i]-1]\) 这个区间中的最长 \(border\) 的长度 ,在一想,这不就是 $\pi[pi[i]-1] $ 嘛?(由于 \(\pi\) 函数,表明的必定是这个区间最长的 \(border\) 的长度)

因此这时咱们只须要不停地跳 \(\pi\) 函数,就能够获得当前的 \(\pi[i+1]\) 了。

Code:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	//由于下标从0开始,因此下标实际上是长度-1,因此格式与上文可能有些不符合,可是理解了就对了!
	for(register int i=1;i<len;++i){
		int j=pi[i-1];
		while(j&&S[i]!=S[j])	j=pi[j-1];
		if(S[i]==S[j]) ++j;
		pi[i]=j;
	}
	return;
}

发现:咱们枚举的 \(i\) 最多让 \(j\) 增长 \(n\),而咱们每次的跳至少会让 \(j-1\),因此不管 \(j\) 减少多少次,总的次数也不会超过 \(O(n)\)

因此此时构造的时间复杂度就为 \(O(n)\)

前缀函数的应用~

1.经典字符串匹配

求一个字符串 \(A\) ,在另外一个字符串 \(B\) 中的出现次数。

在前面 \(Z\) 函数匹配字符串的启发下,很快就能想到:仍是将 \(A\) 拼到 \(B\) 前面,中间加上一个特殊字符 '#' 。

由于有一个 ‘#‘ 在中间,因此全部的 \(\pi[i]\) 必定是 \(\leq\) \(len_A\) 的。一样的想法:那么如何判断 \(A\)\(B\)中出现过呢?

既然 \(\pi[i]\) 表示的是以 \(i\) 为右端点的前缀长度,这个时候 \(A\) 为整个串的前缀,那么对于一个位置 \(i\),当 \(\pi[i]==len_A\) 时,表明着 \(S[i-len_A+1......i]\)\(A\) 相同 。

学会了这个你就能够 \(A\) 下面的例题了!

例题:
P3375 【模板】KMP字符串匹配
CF126B Password
UVA12604 Caesar Cipher

一道字符串匹配的变式吧。。:

P6080 [USACO05DEC]Cow Patterns G

在不少普通的字符串匹配中,\(\pi\) 函数表示的是前缀中最长的 \(border\) ,也就是前缀中先后缀相等的最长长度。

但在这道题中,很明显,没法用相等来表示。

首先,将模式串(\(K\) )和数字串(\(N\))拼起来,中间插入一个特殊符号 “#”。

根据题意:咱们应该将 \(\pi\) 函数中的“相等”看作大小关系相同,因而$ \pi[i]$ 就表示当前 \(S[0\)~\(i]\) 中先后缀大小关系最长的长度,由于有个特殊符号 “#” ,因此全部的 \(\pi[i] \leq K\),而知足“坏蛋团体”区间的右端点,必定知足 \(\pi[r]=K\)

那么这时问题就出在了如何判断大小关系相同了。

若是说当前 \(S[0\)~\(j-1]\)\(S[i-j,i-1]\) 大小关系相同。

那么对于 \(j\)\(i\) 这两个位置,(首先匹配时这个 \(j\) ,必定是\(\leq K\)的)

若是说 \([0,j-1]\) 中 比\(j\) 大的数与\([i-j,i-1]\)中比 \(i\) 大的数的个数相等

并且 \([0,j-1]\) 中 和\(j\) 相等的数与\([i-j,i-1]\)中和 \(i\) 相等的数的个数相等

又由于两个区间长度是同样的,那么区间中大于 \(j\) ,与大于 \(i\) 的数的个数也是相等的。

那么这\([0,j]\)\([i-j,i]\)两个区间的大小关系相等。

如此咱们只须要用一个桶的前缀和,就能够在 \(O(S)\) 的复杂度中求出区间中比它小的与相等的数的个数了。

Warning : 最后须要的是左端点,但利用 \(\pi\) 函数判断的话,符合条件的是右端点.

Code:

与它类似的一道题:CF471D MUH and Cube Walls

2.判断循环节:

\(Z\) 函数差很少,整个前缀函数判断循环节也是经过不断地判断合法的 \(border\) 来肯定周期长度,从而肯定循环节长度的。

可是其实有一个定理(最长循环串长度=总长度-最长相同先后缀长度(前提是这个长度合法,不合法则不存在合法的循环节))

可是因为 \(Z\) 函数的定义,因此 \(Z\) 函数并不能像前缀函数这样 \(O(1)\) 求出最长循环节。

证实用的反证法。。这里就不放了。。。有须要的能够找我。。。

3.一个字符串中本质不一样的子串个数

给定一个长度为 \(n\) 的字符串 \(S\) ,咱们但愿计算它的本质不一样子串的数目。

咱们将用一种在 \(S\) 的末尾添加一个字符后从新计算该数目的方法。

\(k\) 为当前 \(S\) 的本质不一样子串的数量。咱们添加一个新的字符 \(c\)\(S\) 中。现然会有一些写的子串以 \(c\) 结尾而且以前没有出现过,咱们须要对这些字符串基数。

构造一个字符串 \(T+S+c\) 将它反转获得 \(T'\)。如今咱们的任务变成了计算有多少个 \(T'\) 的前缀没有在 \(T'\) 中的其余地方出现过,若是咱们计算了 \(T'\) 的前缀函数的最大值 \(\pi_max\),那么最长的没有在 \(S\) 中的前缀的长度就为 \(\pi_max\)。那么天然,全部更短的前缀也会出现

因此,当添加了一个新字符后出现的新字符串为 \(|S|+1-\pi_max\)

因此对于每次加入的字符,咱们能够 \(O(n)\) 的算出新出现的子串的数量,因此最终复杂度就为 \(O(n^2)\)

这一段抄的老师的讲义。。。(由于我描述不到这么详细,我太弱了)

相关文章
相关标签/搜索