kmp&exkmp 学习笔记

字符串系列:

前言:

kmp绝对™最恶心人的算法了,为什么要初学者学这种恶臭玩意。哦对,大家为什么 vp CF 都很有实力唔。

欠的账最后还是要还的。

五、kmp&exkmp学习笔记+杂题

相关题单:戳我

1.kmp

作为最经典的字符串匹配入门算法,实际上 \(kmp\) 的抽象程度是相当足的,我觉得字符串第一个学 \(kmp\) 还是太抽象了。

(1)字符串的前缀函数

这里的前缀函数实际上就是再代码实现中的 \(failxt\)\(fail\) 数组,它代表的是对于一个字符串,其真前缀和真后缀最大的相同长度(真前缀和真后缀就是前缀和后缀不能等于原串)。

举个例子,对于字符串 ababbaa (下标默认从1开始):

fail[1]=0,因为对于字符串 a 它没有真前缀后真后缀;

fail[2]=0,字符串 ab 没有相同的前后缀;

fail[3]=1,字符串 aba 有真前后缀 a ,长度为1;

fail[4]=2,字符串 abab 有真前后缀 ab ,长度为2;

fail[5]=0,字符串 ababb 没有相同的前后缀;

fail[6]=1,字符串 ababba 有真前后缀 a;

fail[7]=1,字符串 ababbaa 有真前后缀 a。

所以字符串 ababbaa 的前缀函数就是 \([0,0,2,0,1,1]\)

前缀函数还是比较具象的。

(2)推导前缀函数(ノ*・ω・)ノ

下标还是从1开始,由于只有单个字符的时候它是没有真前后缀的,所以 fail[1]=0。

求前缀函数每一位上都不会从头重新开始枚举,而是根据我已经得知的数据,从某个特定的位置开始匹配;而对于模式串的每一位,都有唯一的“特定变化位置”,这个在失配之后的特定变化位置可以帮助我们利用已有的数据不用从头匹配,从而节约时间。

给出代码再结合样例应该会比较好理解一点:

	for(int i=2,j=0;i<=m;i++)//循环从2开始,因为fail[1]=0
	{
		while(j&&b[i]!=b[j+1]) j=fail[j];//当前的j还有值,并且b[i]还不等于b[j+1],跳fail
		if(b[i]==b[j+1]) ++j;//两个相同j++
		fail[i]=j;//这一位的fail就等于j
	}

还是以上面的字符串 ababbaa 为例:

已经初始化 fail[1]=0,当前字符串为 a

向后遍历,加入字符 b ,字符串变为 ab ,此时由于 j=0,进不了while中 ,所以比较 0 后面的那个字符和当前字符。也就是比较 ab ,显然不一样,所以 fail[2] 不变 还是为 0 。

继续向后遍历,加入字符 a ,字符串变为 aba ,此时 j=0,进不了 while 中 ,比较0后面的那个字符和当前字符。也就是比较 aa ,一样了,匹配值加上1,此时的 fail[3]=1 。

加入字符 b,字符串 abba,此时 j=1,终于可以进入 while 了,但是i=4,j+1=2,2与4上都是字符 b 。所以跳出 while,++j,fail[4]=2。

字符串 ababb ,此时j=2,i=5,但是此时下标为3与5的字符分别是 ab 。不相同,调到 fail[2],此时j=0,跳出循环,并且小标1与5的字符分别也是 ab 。所以 \(j\) 不变 fail[5]=0。

后面都是一样的,其实求前缀函数就是将自己和自己匹配。

(3)kmp匹配字符串的应用

这个时候我们就需要将已经求出前缀函数的字符串与另外一个字符串进行匹配了。

考虑一组样例:

abcab
abcacababcab

此时我们已经将上面那个字符串的fail数组求出来了,可以手算模拟出 \(fail[1]=0,fail[2]=0,fail[3]=0,fail[4]=1,fail[5]=2\)

这下就要从下标为1的地方开始匹配了,前四位按位匹配成功,遇到第五位不同,此时查询fail[4]=1,就代表了我们需要将字符串下标为1的地方移到下标为4的地方,这样就变成了。

      abcab
abcacababcab

这样操作实现的代码(在 \(a\) 字符串中找 \(b\) 字符串是否出现,先处理好 \(b\) 字符串的fail数组):

	for(int i=1,j=0;i<=n;i++)
	{
		while(j&&a[i]!=b[j+1]) j=fail[j];//匹配失败就向前跳
		if(a[i]==b[j+1]) ++j;//如果相同j++
		if(j==m)//j的值等于b字符串的长度,那么此时相当于b字符串出现了一次
		{
			......//进行一些操作
		}
	}

这样可以做到快速移动。(说实话你要是看我的讲解就看懂了那就见鬼了,毕竟我只是粗略的模拟了一下kmp匹配算法中进行的操作)。

入门的话可以参阅以下资料

https://oi-wiki.org/string/kmp/

https://www.luogu.com.cn/blog/pks-LOVING/zi-fu-chuan-xue-xi-bi-ji-qian-xi-kmp-xuan-xue-di-dan-mu-shi-chuan-pi-post

https://tom0727.github.io/post/081-kmp/

(4)border

其实前面的kmp字符串匹配代码还是比较好理解的,自己上手画一下操作的过程就比较清晰了。

border就很考验对于字符串的理解了(其实如果你是whk带师的话,每种性质写个几十遍你也成border带师了)

首先对一些记号进行说明:

  • \(\left\vert s \right\vert\) 代表字符串 \(s\) 的长度。

  • \(s_1,s_2,s_3\)表示字符串 \(s\) 下标为 1,2,3时的字符。

  • \(s_{\left\vert l,r \right\vert}\) 代表字符串 \(s\) 从下标 \(l\)\(r\) 的一段,其实就是子串。

  • \(pre(s,i),suf(s,i)\) 代表字符串长度为 \(i\) 的前后缀。

  • border:\(\forall 0\leq i < \left\vert s \right\vert\)\(pre(s,i)=suf(s,i)\),我们就称\(pre(s,i)\)是字符串 \(s\) 的一个border。

  • 周期:对于 \(\forall 0\leq p < \left\vert s \right\vert\)\(\therefore 0\leq i \leq \left\vert s \right\vert-p\) 都有 \(s_i=s_{i+p}\),就称 \(p\) 是字符串 \(s\) 的一个周期。

其实可以显然的发现我们前面求的前缀函数实际上就是一个字符串的最长boader。

通俗一点的讲,若两字符串 \(S,T\) 满足:

  • \(S \neq T\)

  • \(S\) 既是 \(T\) 的前缀又是后缀 .

则称 \(S\)\(T\) 的一个 border .

例如:abcdabc的一个border是abc。所以一个字符串的border可能有多个。

哎,我还是太菜了,直接将我知道的border的性质放在这里吧:

border 和周期:

  • a.如果 \(pre(s,i)\)\(s\) 的 border,那么 \(\left\vert s \right\vert-i\)\(s\) 的一个周期。(用的最多的一个性质,因为和前缀函数有莫大的关系)

  • b.如果 \(p,q\) 都是 \(s\) 的周期,并且 \(p+q\leq p\left\vert s \right\vert\),那么 \(gcd(p,q)\) 也是 \(s\) 的一个周期。

  • c.如果 \(p,q\) 都是 \(s\) 的周期,并且 \(p+q-gcd(p,q)\leq p\left\vert s \right\vert\),那么 \(gcd(p,q)\) 也是 \(s\) 的一个周期。(其实只需要记上面那个就可以了)。

  • d.若 \(s\)\(t\) 的前缀,且 \(t\) 有周期 \(a\)\(s\) 有整周期 \(b\)(整周期就是 \(b\) 可以整除 \(\left\vert s \right\vert\)),且 \(b | a\)(也就是 \(b\) 可以整除 \(a\)),\(\left\vert s \right\vert \ge a\),则 \(t\) 也有周期 \(b\)

  • e.若 \(s\) 如下图匹配,则表示 \(s1 \cup s2\) 有长度为 \(d\) 的周期 见图

image

字符串匹配:

  • a.若两字符串 \(s,t\) 满足 \(2\left\vert s \right\vert \ge \left\vert t \right\vert\) ,那么 \(u\)\(v\) 中匹配的位置是一个公差为 \(pre(u)\) 的等差数列,并且此时 \(v\)\(u\) 的最小周期的重复(什么玩意,我也不清楚,但是好像没有什么实际用处,可以略过)。

  • b.字符串 \(s\) 所有不小于\(\left\vert s \right\vert /2\) 的 borde r的长度构成一个等差数列。

  • c.字符串 \(s\) 的所有 border 长度排序后可分成 \(O(log\left\vert s \right\vert)\) 段, 每段是一个等差数列。

border 的其他性质(比如和kmp的联系):

  • a.对于任意一个字符串 \(s\) ,一个border的长度就对应一个border(比如 abcdabc 的长度为3的border当然就只能是 abc )。并且,假设 \(s\) 长度记为 \(n\),则 \(s\) 的所有border的长度分别为:\(fail[n], fail[fail[n]], fail[fail[fail[n]]].......\)直到值为0的不算。并且这个序列的值从左往右递减(根据kmp的性质容易得出)。

  • b.对一个字符串 \(s\) 求解fail数组之后,我们就知道 \(s\) 所有前缀的所有border了。

我应该是可以死了,但是有些丧心病狂的出题人真的会出这些与border有关的性质,反正我觉得我碰上就是死,趁早用hash去打部分分。

(4)习题:

其实理解了kmp算法与boader的本质之后,有些性质还是比较显然的。

P3375 【模板】KMP

板子题,给你两个字符串,问你其中一个字符串在另外一个字符串出现的位置,并且输出 \(fail\) 数组。

谔谔,直接套板子,先将 \(b\)\(fail\) 数组求出来,然后在 \(a\) 中找。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,m,fail[M];
char a[M],b[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>a+1>>b+1;n=strlen(a+1),m=strlen(b+1);
	for(int i=2,j=0;i<=m;i++)//下标从2开始,默认fail[1]=0
	{
		while(j&&b[i]!=b[j+1]) j=fail[j];//不断跳fail进行匹配
		if(b[i]==b[j+1]) ++j;
		fail[i]=j;//处理fail数组
	}
	for(int i=1,j=0;i<=n;i++)//去匹配其他字符串的时候下标从1开始
	{
		while(j&&a[i]!=b[j+1]) j=fail[j];//之前是自己和自己匹配,现在是自己和a匹配,所以是a[i]!=b[j+1]
		if(a[i]==b[j+1]) ++j;
		if(j==m)//如果j=m,那么就说明了当前两个字符串相同的长度为m了,也就相当于b字符串出现了一次
		{
			cout<<i-m+1<<"\n";//要求输出的是字符串的开头下标
		}
	}
	for(int i=1;i<=m;i++) cout<<fail[i]<<" ";//输出fail 数组
	return 0;
}

CF1029A Many Equal Substrings

其实就是给你一个字符串 \(t\) ,你需要构造一个字符串 \(s\),使得字符串 \(s\) 中出现 \(k\) 次字符串 \(t\),你需要输出满足条件的长度最小的字符串 \(s\)

谔谔,其实 \(fail\) 数组的性质挺多的(再说一遍哒:这篇博文中的 \(fail\) 数组实际上就是 \(next\) 数组,只是学了ACAM之后喜欢将数组变量名统一起来,所以就称 \(fail\) 数组了)。这道题我们就需要运用其中的一个性质:如果 \(len%(len-fail[len])==0\) 就说明有循环节,\(len-fail[len]\) 的值,就是 \(s\) 的最小循环节的长度,而 \(len/(len-next[len])\) 就是最大循环次数。

其实这是比较好理解的,前面说过,如果 \(pre(s,i)\)\(s\) 的 border,那么 \(\left\vert s \right\vert-i\)\(s\) 的一个周期。而我们求的fail数组就是最大的border,border最大,总长度减去border得到的周期自然就是最小的。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n,k,fail[M];
char s[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	cin>>s+1;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}//处理出fail数组
	cout<<s+1;//这里面包含了一个t
	for(int i=1;i<k;i++)
	{
		cout<<s+fail[n]+1;//输出这个最小的周期,输出k-1次
	}
	return 0;
}

CF126B Password

其实很多kmp的难度都评低了吧(不然我就汗流浃背了)。简明题意,给你一个字符串,你需要找到既是 \(s\) 的前缀,又是 \(s\) 的后缀同时再 \(s\) 的中间出现过的最长的一段子串,如果这样的子串没有那么输出 Just a legend

首先先只关注即是 \(s\) 的前缀,又是 \(s\) 的后缀这一个限制,那么最长的不就是 \(fail[n]\) 嘛(其中 \(n\) 是字符串 \(s\) 的长度),依照这这个思路,我们就可以很自然的想到使用kmp来处理了,但是怎么去判断字符串中间也出现前后缀。

我们可以找到一个符合条件的\(s_{\left\vert l,r \right\vert}\),可以看出,\(s_{\left\vert l,r \right\vert}\)\(s_{\left\vert 1,r \right\vert}\)的前后缀,还是\(s_{\left\vert l,n \right\vert}\)的前后缀。那么对 \(s\) 处理好 fail 数组之后我们就可以枚举每一位观察是否符合条件。

具体的操作步骤就是将 \(s\) 做一遍kmp,再将 \(s\) 颠倒过来再做一遍kmp,正着的相当于找的是前缀,反着相当于找的是后缀,如果前缀和后缀在同一下标上的 fail 值相同,那么就说明这个字符是符合要求的,此时的长度就是 fail 数组存的值,找到最大值并记录下位置。

以样例一为例, fixprefixsuffix 反过来是 xiffusxiferpxif 处理出正着的 fail 数组是 \([0,0,0,0,0,1,2,3,0,0,1,1,2,3,0]\),反着的是 \([0,0,0,0,0,1,2,3,0,0,0,1,2,3,0]\),每一次判断 \(fail[0][i]==fail[1][n-i+fail[0][i]]\) 就可以验证前面所说的四个前后缀关系了。(其实是可以用哈希乱搞的)

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
char a[M];
int fail[2][M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>a+1;int n=strlen(a+1);
	for(int i=2,j=0;i<=n;i++)//正着的求一次fail数组
	{
		while(j&&a[i+1]!=a[j+1]) j=fail[0][j];
		if(a[i+1]==a[j+1]) ++j;
		fail[0][i+1]=j;
	}
	reverse(a+1,a+n+1);//翻转字符串
	for(int i=2,j=0;i<=n;i++)//反着的求一次fail数组
	{
		while(j&&a[i+1]!=a[j+1]) j=fail[1][j];
		if(a[i+1]==a[j+1]) ++j;
		fail[1][i+1]=j;
	}
	reverse(a+1,a+n+1);//转回来,等会要输出答案
	int pos=0,maxx=0;
	for(int i=1;i<=n;i++)
	{
		if(fail[0][i]==fail[1][n-i+fail[0][i]])
		{
			if(fail[0][i]>maxx) maxx=fail[0][i],pos=i;//判断是否满足四种前后缀关系,找出最长的子串,记录下标
		}
	}
	if(!pos) cout<<"Just a legend\n";//无解
	else
	{
		for(int i=pos-maxx+1;i<=pos;i++) cout<<a[i];//输出答案,我们记录的是子串的结尾
	}
	return 0;
}

AT_abc284_f ABCBAC

这不是hash大水题?只是需要一点的空间模拟能力,首先只有三种操作,然后对于一个字符串 \(s\) 进行这三种操作后使其等于字符串 \(t\)。三种操作分别是:

  • \(S\) 的前 \(i\) 个字符;
  • \(S\) 翻转得到的字符串;
  • \(S\) 的后 \(N-i\) 个字符。

相当于就是在字符串 \(s\) 中插入一个反转的字符串 \(s\) 使其等于 \(t\)。用hash枚举每一位判断是否合法,时间复杂度\(O(n)\)。细节见代码。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e6+5,base=19260817,mod=998244353;//这题好像卡自然溢出
int n,m ;
char s[M],t[M];
int f[M],sum1[M],sum2[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=m;i++) f[i]=f[i-1]*base%mod;
}//处理进制次方

inline int get1(int l,int r)
{
	return (sum1[r]-sum1[l-1]*f[r-l+1]%mod+mod)%mod;
}

inline int get2(int l,int r)
{
	return (sum2[l]-sum2[r+1]*f[r-l+1]%mod+mod)%mod;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;m=2*n;pre();
	cin>>t+1;
	for(int i=1;i<=m;i++) sum1[i]=sum1[i-1]*base%mod+t[i];
	for(int i=m+1;i>=1;i--) sum2[i]=sum2[i+1]*base%mod+t[i];//正着反着各做一次hash
	if(get1(1,n)==get2(n+1,m))//特判一下直接将字符串反转+原字符串是否合法
	{
		cout<<t+n+1<<"\n";
		cout<<"0\n";
		return 0;
	}
	//三种操作的范围是1-i,接着是i+1-i+n,然后是i+n+1-m
	//由于中间是翻转了的,所以实际上我么需要让pre(1,i)==suf(i+n-i+1,i+n) 
	//剩下的pre(i+n+1,m)==suf(i+1,n)同理 
	for(int i=1;i<=n;i++)
	{
		if(get1(1,i)==get2(n+1,i+n)&&get2(i+1,n)==get1(i+n+1,m))
		{
			for(int j=1;j<=i;j++) cout<<t[j];
			for(int j=i+n+1;j<=m;j++) cout<<t[j];
			cout<<"\n";
			cout<<i<<"\n";
			return 0;
		}
	}
	cout<<"-1\n";//无解输出
	return 0;
}

但是用hash秒了这道题之后,这道题也确实可以用kmp来做(艹,为什么题解区大佬还有用string水过去了)。那这题用kmp做有点太麻烦了,具体的做法是将字符串 \(t\) 分成两截,每一截的长度为 \(n\),然后翻转其中的一截,延长两倍,然后进行kmp的字符串匹配。

举个例子(样例一):abcbac 分成 abc 和 bac 两部分,然后将后一部分翻转再加倍,变成了 cabcab ,然后用前面一截 abc 去匹配。匹配成功就是找到了,失败就是无解咯。细节还挺多的。(我没写完!只是写了匹配的过程,后面的输出自己写哒)。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e6+5;
int n;
char s[M],t[M],a[M];//设前一截为s,后一截为t a是原串 
int fail[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	cin>>a+1;int cnt=n;
	for(int i=1;i<=n;i++) s[i]=a[i];
	for(int i=n;i>=1;i--) t[i]=a[++cnt],t[i+n]=t[i];//翻转并翻倍 
	for(int i=2,j=0;i<=n;i++)//处理s的fail数组 
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}//板子
	for(int i=1,j=0;i<=2*n;i++)//两个字符串进行匹配 
	{
		while(j&&t[i]!=s[j+1]) j=fail[j];
		if(t[i]==s[j+1])  j++;
		if(j==n)
		{
			...//麻烦死咯,现在是匹配好了,可能会进行一些操作
			return 0;
		}
	}
	cout<<"-1\n";
	return 0;
}

P4391 [BOI2009] Radio Transmission 无线传输

这就和周期有关系了,题目给出一个字符串 \(s_1\) ,并说 \(s_1\) 是由某个字符串 \(s_2\) 不断自我连接形成的(实际上就是以 \(s_2\) 为循环节/周期嘛),问你 \(s_2\) 的最短长度。

参照前面 boader 的性质 如果 \(pre(s,i)\)\(s\) 的 border,那么 \(\left\vert s \right\vert-i\)\(s\) 的一个周期。询问最小的周期那么我们就需要找到最长的 border,前缀函数求得就是最长的 border。那这题就做完了,只需要使用板子,自己匹配自己,处理出 fail 数组,然后用 \(n-fail[n]\)\(n\) 是字符串 \(s_1\) 的长度)就是最后的答案了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,fail[M];
char s[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;cin>>s+1;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) j++;
		fail[i]=j;
	}//板子
	cout<<n-fail[n]<<"\n";
	return 0;
}

CF54D Writing a Song

翻译的很好唔,求一个字符串 \(s\) ,满足该串长度为 \(n\),只出现字母表中前 \(k\) 个字母,并且在指定位置必须出现指定字符串 \(p\)

直接模拟,对于当前位是0的就跳过,对于当前位是1的就向后复制一个指定的字符串 \(p\),如果长度超过 \(n\) 那么就无解,然后再对于0进行讨论,如果这一位上有字符,那么匹配一下,如果与指定的字符串 \(p\) 相同了就无解,如果这一位上没有字符,那么可以填上一个与 \(p[1]\)(我已经习惯了用1作为下标开头orz)不相同的字符。

不对呀,这题好像不用kmp,数据范围太小了,足够乱搞。

代码实现还是有点繁琐:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=105;
int n,k,len1,len2;
char s[M],t[M],a[M],opt;

inline int check(int x)
{
	for(int i=x;i<=x+len1-1;i++)
	{
		if(s[i-x+1]!=a[i]) return 0;
	}
	return 1;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	cin>>s+1;
	cin>>t+1;
	len1=strlen(s+1),len2=strlen(t+1);
	for(int i=0;i<k;i++)//钦定一个与s[1]不相同的字符来填空位 
	{
		if(s[1]-'a'!=i)
		{
			opt=i+'a';break;
		}
	}
	for(int i=1;i<=len2;i++)
	{
		if(t[i]=='1')
		{
			for(int j=i;j<=i+len1-1;j++)
			{
				if(!a[j]) a[j]=s[j-i+1];
				if(a[j]&&a[j]!=s[j-i+1])
				{
					cout<<"No solution\n";
					return 0;
				}
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		if(!a[i])//这一位上空空如也 
		{
			a[i]=opt;continue;
		}
		if(t[i]=='0')
		{
			if(check(i))//哦吼,匹配上了,矛盾 
			{
				cout<<"No solution\n";
				return 0;
			}
		}
	}
	cout<<a+1<<"\n";
	return 0;
}

你交了上面这份代码你会发现其实它是错的,因为有一组数据是:

7 2
aba
10001

如果我们在空位上填上 b 反而会构造出一个 aba 所以还需要进行一些判重操作

AC代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e2+5;
using namespace std;
char c[M],k[M],l[M];
int n,j,h,w,q,x;
signed main()
{
	cin>>h>>w>>c+1>>k+1;
	x=strlen(c+1);
	w=strlen(k+1);
	for(n=1;n<=w;n++)
	{
		if(k[n]=='1')
			for(q=1;q<=x;q++)
			{
				if(l[n+q-1]&&l[n+q-1]!=c[q])
				{
					cout<<"No solution";
					return 0;
				}
				l[n+q-1]=c[q];
			}
	}
	for(n=1;n<=h;n++)
	{
		if(k[n]=='0')
		{
			for(q=1;q<=x;q++)
			{
				if(l[n+q-1]&&l[n+q-1]!=c[q]) break;
			}
			if(q>x)
			{
				for(q=1;q<=x;q++)
					if(!l[n+q-1]) 
					{ 
						l[n+q-1]=((c[q]=='a')?'b':'a');
						break;
					}
				if(q>x)
				{
					cout<<"No solution";
					return 0;
				}
			}
		}
	}
	for(n=1;n<=h;n++)
	{
		if(!l[n])cout<<'a';
		else cout<<l[n];
	}
	return 0;
}

CF25E Test

其实这道题反而思维难度并不是很高,你只需要枚举 \(a,b,c\) 三个字符串怎么拼接就可以了,最后也就6中可能的拼接方案,其中有一种特殊情况,你需要判断当前加进来的字符串是否被前面已经拼好的字符串包含,如果没有就继续操作,否则就不管(因为已经包含了)。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<vector>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5;
int fail[M],ans=1e9;

vector<int> KMP(string s)
{
	int n=s.size();
	vector<int>pi(n);
	for(int i=1,j=0;i<n;i++)
	{
		if(s[i]==s[j]){pi[i]=++j;}
		else if(j==0){pi[i]=0;}
		else{j=pi[j-1];i--;}
	}
	return pi;
}

inline int check(string a,string b,string c)
{
	string fake,kk,x;
	kk=a;
	fake = b+'#'+a;
	vector<int>v=KMP(fake);
	int flag=0;
	for(auto e:v)
	{
		if(e==b.size())
		{
			flag=1;
		}
	} 
	if(!flag)
	{
		kk+=b.substr(v[v.size()-1],b.size());
	}
	fake=c+'#'+kk;
	vector<int>v1=KMP(fake);
	flag=0;
	for(auto e:v1)
	{
		if(e==c.size()) flag=1;
	}
	if(!flag)
	{
		kk+=c.substr(v1[v1.size()-1],c.size());
	}
	return kk.size();
}

inline void solve()
{
    string a,b,c;
	cin>>a>>b>>c;
	int ans=1e9;
	ans=min(ans,check(a,b,c));
	ans=min(ans,check(a,c,b));
	ans=min(ans,check(b,a,c));
	ans=min(ans,check(b,c,a));
	ans=min(ans,check(c,a,b));
	ans=min(ans,check(c,b,a));
	cout<<ans<<"\n";
}


signed main()
{
	ios_base::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

SP7155 CF25E - Test

上一题的双倍经验,只是换成多组询问。

P8085 [COCI2011-2012#4] KRIPTOGRAM

挺有趣的一道题,可以用hash+队列水过去,甚至hash都不用,直接借助STL就可以做。

对于这题的kmp做法其实和哈希做法差不多,还是要运用hash的思想将一段区间内的字符串全部转化为字符,然后用kmp进行匹配。思路还是挺好想的,代码实现起来可能有点麻烦。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,m,fail[M],a[M],b[M],s[M],t[M],pos,cnt,tot;
map<string,int> mp;
map<string,int> dc;
map<string,int> mp_2;//使用map来进行映射,将字符串转化为数字
string s1[M],s2;
signed main()
{
	while(cin>>s2)
	{
		if(s2=="$") break;
		n++;
		s1[n]=s2;
	}
	while(cin>>s2)
	{
		if(s2=="$") break;
		m++;
		if(mp_2[s2]==0)
		{
			tot++;
			mp_2[s2]=tot;
			b[m]=tot;
		}
		else b[m]=mp_2[s2];
		if(m!=1) t[m-1]=b[m]-b[m-1];
	}
	for(int i=1;i<=n;i++)
	{
		if(mp[s1[i]]==0)
		{
			cnt++;
			dc[s1[i]]=cnt;
			a[i]=cnt;
		}
		else a[i]=dc[s1[i]];
		mp[s1[i]]++;
		if(i-m+1>=1) mp[s1[i-m+1]]--;
		if(i!=1) s[i-1]=a[i]-a[i-1];
	}
	if(m==1)
	{
		cout<<1<<endl;
		return 0;
	}
	for(int i=1,j=0;i<m-1;i++)
	{
		while(j&&t[i+1]!=t[j+1]) j=fail[j];
		if(t[i+1]==t[j+1])
		{
			j++;
		}
		fail[i+1]=j;
	}//kmp匹配的板子
	for(int i=0,j=0;i<n-1;i++)
	{
		while(j&&s[i+1]!=t[j+1]) j=fail[j];
		if(t[j+1]==s[i+1]) j++;
		if(j==m-1)
		{
			pos=i-m+2;
			break;
		}
	}
	cout<<pos+1<<endl;
	return 0;
}

P3435 [POI2006] OKR-Periods of Words

这道题就需要运用 boader 与前缀函数的性质了。对于一个仅含小写字母的字符串 \(a\)\(p\)\(a\) 的前缀且 \(p\ne a\),那么我们称 \(p\)\(a\) 的 proper 前缀。

规定字符串 \(Q\) 表示 \(a\) 的周期,当且仅当 \(Q\)\(a\) 的 proper 前缀且 \(a\)\(Q+Q\) 的前缀。若这样的字符串不存在,则 \(a\) 的周期为空串。例如 ababab 的一个周期,因为 ababab 的 proper 前缀,且 ababab+ab 的前缀。求给定字符串所有前缀的最大周期长度之和。

手玩一下样例:

8
babababa

i=1,b 没有周期

i=2,ab也没有符合条件的周期

i=3,aba有proper前缀ab,而aba又是ab+ab的前缀,符合要求,最长周期为2

i=4,同上,最长周期为2

i=5,ababa有proper前缀abab,而ababa又是abab+abab的前缀,最长周期为4

i=6,同上

i=7,符合要求的最长周期是ababab,长度为6

i=8,同上。

答案就是2+2+4+4+6+6=24,这就是样例。但是推了半天似乎没有什么特殊性质,题目中要求我们找出最长周期,根据性质:如果 \(pre(s,i)\)\(s\) 的 border,那么 \(\left\vert s \right\vert-i\)\(s\) 的一个周期,相当于是要我们找出最小的boader,但是前缀函数 fail 中记录的都是最长的 border。

那我们再借助一个性质:对于任意一个字符串\(s\),一个border的长度就对应一个border(比如 abcdabc 的长度为3的border当然就只能是 abc )。并且,假设 \(s\) 长度记为 \(n\),则 \(s\) 的所有border的长度分别为:\(fail[n], fail[fail[n]], fail[fail[fail[n]]].......\)直到值为0的不算。并且这个序列的值从左往右递减。

这下终于可以做了,我们可以不断地向前跳 \(fail\),直到 \(fail\) 为0的前一个 \(fail\) 值就是最小的 border,为了加快程序的运行速度,每一次找了之后我们就把当前的 \(fail\) 值改成找到的最小 border,后面的 \(fail\) 跳到当前这一位就不用重复的向前跳了。

(记一下性质还是挺有用的)

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,fail[M];
char s[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;cin>>s+1;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) j++;
		fail[i]=j;
	}//先将字符串的fail数组处理出来
	int ans=0;
	for(int i=2,j=2;i<=n;i++,j=i)
	{
		while(fail[j]) j=fail[j];//每一位的fail都向前跳,直到为0
		if(fail[i]) fail[i]=j;//然后修改
		ans+=i-j;//每一个前缀最长的周期就是当前前缀的长度减去最小的boader
	}
	cout<<ans<<"\n";
	return 0;
}

CF1200E Compress Words

给出 \(n\) 个字符串,你需要将它们全部连在一起,其中如果前一个字符串的后缀于后一个字符串的前缀有重复,你需要将它们合并起来。(还是比较直观的)。

这是 kmp 中一个非常常见的 trick,由于我们需要找到前后缀的最长相同部分,联系 kmp 不也是求字符串前后缀的最长重复长度嘛。我们设前一个字符串为 \(a\),后一个字符串为 \(b\),那么 \(a+b\) 求的是 \(a\) 的前缀与 \(b\) 的后缀的最长长度,但我们需要知道的是 \(a\) 的后缀与 \(b\) 的前缀的重复长度,我们只需求 \(b+a\) 的fail数组即可。(对于这种有字符串拼接的,string可能会方便许多)。

那么对于每一个字符串,暴力的进行拼接似乎有点问题,比如一个非常长的字符串后面拼上很多个单字符,每一次的时间复杂度还是 \(O(n+m)\)的,考虑优化,前缀后缀的长度肯定不超过较短字符串的长度,那么我们只需要截取较长字符串的长度为较短字符串的前缀/后缀。

代码:

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
const int M=1e6+5;

int n; 
string s,ans,cur;
int fail[M];

inline int kmp(string s)
{
	int n=s.size();
	s=' '+s;//前面加一个空格使得下标从1开始
	fail[1]=fail[0]=0;
	for(int i=2,j=0;i<=n;++i)//自己与自己匹配
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}
	return fail[n];
} //板子

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i)
	{
		cin>>s;
		if(i==1) ans+=s;//初始化ans,第一个字符串不处理
		else
		{
			int minn=min(s.size(),ans.size());//长度不超过较短字符串的长度(其实这里默认了ans较长ing)
			int len=kmp(s+'#'+ans.substr(ans.size()-minn,minn));
			for(int j=len;j<s.size();++j)
			{
				ans+=s[j];
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

P4824 [USACO15FEB] Censoring S

给定两个字符串,\(s\)\(t\) ,你需要将所有在 \(s\) 中出现的 \(t\) 全部删去,注意:每次删除一个 \(t\) 后,可能会出现一个新的 \(t\),删去之后两段拼接起来又形成了一个 \(t\)

举个例子:

aaabbab
ab

删除一遍之后变成:aab,这时候增加了一个ab,所以最后的答案是a

这个时候如果我们一直while,不断删除 \(t\),时间复杂度感觉会炸。数据范围告诉我们最好使用 \(O(n)\)级别的算法,相当于我们只可以做一次 kmp。

那么首先还是将 \(t\) 的 fail 数组处理好,接着我们可以发现其实这种删除方式可以使用栈模拟还是上面的例子,a入栈,a入栈,b入栈,发现 ab 出现,将ab弹出去,现在栈内只有a,然后b入栈,又出现ab,将ab弹出去,栈空了,后面的同理,最后发现栈内就只有a,那这就是最后的答案。

为了使弹出栈之后可以与后面的继续匹配,除了记录当前这一位的字符,我们还需要将这一位的 \(fail\) 数组记录下来,用一个 pair 即可,每一次弹出之后,\(j\) 当然就更新为栈顶的 \(fail\) 数组,然后向后匹配即可。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack> 
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int lena,lenb;
char a[M],b[M],ans[M];//这里面的a就是s,b就是t
int fail[M],f[M];
stack<pair<char,int> > s;//使用STL,懒得手写,使用pair将字符和fail值合在一起

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>a+1>>b+1;
	lena=strlen(a+1),lenb=strlen(b+1);
	for(int i=2,j=0;i<=lenb;i++)
	{
		while(j&&b[i]!=b[j+1]) j=fail[j];
		if(b[i]==b[j+1]) j++;
		fail[i]=j;
	}//处理fail数组
	s.push(make_pair(0,0));//加一个空元素防止在栈为空的时候查询时RE,栈都为空了,j自然就是0咯
	for(int i=1,j=0;i<=lena;i++)
	{
		while(j&&a[i]!=b[j+1]) j=fail[j];
		if(a[i]==b[j+1]) j++;
		f[i]=j;
		s.push(make_pair(a[i],f[i]));//非常正常的匹配,将每一个元素扔入栈中
		if(f[i]==lenb)//如果匹配上b了
		{
			for(int k=1;k<=lenb;k++) s.pop();//弹出b
			j=s.top().second;//j就是栈顶的fail值
		}
	}
	int num=s.size();
	for(int i=1;i<num;i++) ans[i]=s.top().first,s.pop();
	for(int i=num-1;i>=1;i--) cout<<ans[i];//将栈中的字符输出
	return 0;
}

P5829 【模板】失配树

非常有趣的性质,我们将一个字符串处理好 \(fail\) 数组之后,连边\((fail[i],i)\),最后会形成一颗树的形式,我们称这棵树为这个字符串的失配树(fail树),这颗树有两个性质(其实好像没啥大用,仅限于了解,失配树连一道习题都没有ing,既然是一颗树我们就可以运用树的优美性质解决。):

\(i\) 的所有祖先都是前缀 \(pre(s,i)\) 的 border;

没有祖先关系的两个点 \(i,j\) 没有border关系。

\(m\) 组询问,每组询问给定 \(p,q\),求 \(s\)\(\boldsymbol{p}\) 前缀 和 \(\boldsymbol{q}\) 前缀 的 最长公共 \(\operatorname{border}\) 的长度。

可以大胆的猜测两个前缀的最长公共 boader 就是失配树上 \(p\) 点和 \(q\) 点的 lca 。差不多算半对,因为如果其中一个点是另外一个点的祖先,那么求得的答案就是其中一个点,代表的就是这个前缀是公共最长的 boader,可是 boader 的定义是真前缀和真后缀的相同部分长度。为了避免这种特殊情况,我们可以求 \(fa_p\)\(fa_q\) 的 lca 的。

求 lca 的方法还是有很多的,倍增,树剖都可以,我喜欢树剖,剖完之后树的所有性质基本上都出来了,而且常数非常小,跑的很快。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,m;
char s[M];

int cnt=0;
struct N{
	int to,next;
};N p[M<<1];
int head[M];

inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}

int deep[M],fa[M],size[M],son[M];
inline void dfs1(int u,int f,int d)
{
	deep[u]=d,size[u]=1,fa[u]=f;
	int maxson=0;
	for(int i=head[u];i!=-1;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		dfs1(v,u,d+1);
		size[u]+=size[v];
		if(size[v]>maxson)
		{
			maxson=size[v],son[u]=v;
		}
	}
}

int top[M],id[M],num;
inline void dfs2(int u,int topp)
{
	id[u]=++num,top[u]=topp;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int i=head[u];i!=-1;i=p[i].next)
	{
		int v=p[i].to;
		if(v==fa[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}

inline int get_lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y]) swap(x,y);
	return x;
}//上面都是树剖的板子

int fail[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	memset(head,-1,sizeof(head));
	cin>>s+1>>m;
	n=strlen(s+1);
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}//先处理fail
	for(int i=1;i<=n;i++) add(fail[i],i);//模拟题意
	dfs1(0,-1,1);
	dfs2(0,0);//树剖两次dfs预处理
	int x,y;
	while(m--)
	{
		cin>>x>>y;
		cout<<get_lca(fa[x],fa[y])<<"\n";//找父亲的lca
	}
	return 0;
}

P2375 [NOI2014] 动物园

md,出题人就是事情多,简化题意,我们需要找到一个长度小于字符串长度一半的 boader (题目中告诉我们不能让相同的前后缀有重叠部分),那么还是利用 boader 的性质:对于任意一个字符串\(s\),一个border的长度就对应一个border(比如 abcdabc 的长度为3的border当然就只能是 abc )。并且,假设 \(s\) 长度记为 \(n\),则 \(s\) 的所有border的长度分别为:\(fail[n], fail[fail[n]], fail[fail[fail[n]]].......\)直到值为0的不算。并且这个序列的值从左往右递减。

不断的跳 \(fail\) 直到 \(fail\) 的值变为 \(0\) 或者 fail 值小于字符串长度的一半。剩下的就是注意多组询问+输出方式

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5,mod=1e9+7;
int q,n;
char a[M];
int fail[M],ans[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>q;
	while(q--)
	{
		cin>>a+1;int n=strlen(a+1);ans[1]=1;
		for(int i=2,j=0;i<=n;i++)
		{
			while(j&&a[i]!=a[j+1]) j=fail[j];
			if(a[i]==a[j+1]) j++;
			fail[i]=j,ans[i]=ans[j]+1;
		}//处理fail
		int sum=1;
		for(int i=2,j=0;i<=n;i++)
		{
			while(j&&a[i]!=a[j+1]) j=fail[j];
			if(a[i]==a[j+1]) j++;
			while((j<<1)>i) j=fail[j];//不断的跳fail直到满足要求
			sum=(sum*(ans[j]+1))%mod;//顺便累加答案
		}
		cout<<sum<<"\n";
	}
	return 0;
}

P3426 [POI2005] SZA-Template

我就没见过这么抽象的题目,题目描述还是非常具象的,你需要用一个印章印一串字符串,同一个位置的相同字符可以印多次,问你印章上字符串长度的最小值。

说实话我也没有理解这题的真正思路(脑子成糨糊了)。

首先唯一可以确定的是,这个印章上的字符串一定是原串的一个 boader,对于每一个字符串它的本质不同的 border 只有 log 种(本质不同值指的是不能互相表示)。

给出代码吧,留到之后来填坑:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5;
int n,f[M],nxt[M],ls[M];
char s[M];
signed main()
{
	cin>>s+1;
	n=strlen(s+1);
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[j+1]!=s[i])j=nxt[j];
		j+=(s[i]==s[j+1]);nxt[i]=j;
	}
	for(int i=1;i<=n;i++)
	{
		f[i]=i;
		if(i-ls[f[nxt[i]]]<=nxt[i])
		{
			f[i]=f[nxt[i]];
		}
		ls[f[i]]=i;
	}
	cout<<f[n]<<"\n"; 
	return 0;
}

留几道习题(我不会告诉你我现在看见字符串就想死,所以之后会填的,现在咕咕咕)。

CF176B Word Cut

CF1721E Prefix Function Queries

AT_abc312_h [ABC312Ex] snukesnuke

P3193 [HNOI2008] GT考试

(我并不打算补这题,太抽象了)。

2.扩展 KMP/exKMP(Z 函数)

前面被抽象完了,现在就来看看放松身心的 exkmp,实际上叫做 exkmp,它和 kmp 其实没有什么关系(它其实和manacher非常像)。非常好理解。

(1)Z函数

定义一个字符串的Z函数 \(z_i\) 表示 \(s\)\(i\) 后缀与 \(s\) 本身的最长公共前缀,一般来说 \(z_1=n\),就是 \(s=s\)嘛(下标还是从1开始)。

举个例子,对于字符串aabaca

\(z[1]=6,z[2]=1,z[3]=0,z[4]=1,z[5]=0,z[6]=1\)

\(z[2]\) 就是求 abacaaabaca 的最长公共前缀,其余同理。

(2)实现方法:

由于是要求最长的公共前缀,暴力做法就是每一个后缀分别和原串一个一个的比,如果不相同了那么匹配结束,z函数的值就是前面相同的字符长度。这种暴力做法对于随机字符串还是跑的挺快的,但是如果字符串是类似 aaaaaa,时间复杂度是 \(O(n^2)\)哒。

类似manacher,利用已经求出来的信息进行处理,维护我们处理出来的最靠右的最长公共前缀记作 \(r\),与其对应的后缀开头记作 \(l\) 假设当前我们需要找出 \(s\)\(i\) 开头的后缀的z函数的值,那么分类讨论:

如果 \(i>r\) 直接按照暴力匹配 \(while(t[1+z[i]]==t[i+z[i]]) ++z[i];\),从1开始和从 \(i\) 开始向后暴力的找
如果 \(i \leq r\),由于 \(s[1,r-l+1]=s[l,r]\),这是 \(l,r\)告诉我们的。所以有\(s[i,r]=s[i-l+1,r-l+1]\),所以以 \(i\) 开头的后缀z函数的值至少都是 \(z_{i-l+1}\),但是有可能越界,所以 \(z_i\) 这时候就是 \(min(r-i+1,z_{i-l+1})\),然后再暴力的去加 \(z_i\)

如果此时 \(i+z[i]-1\geq r\),那么我们就需要更新 \(r\)

直观一点的说:对于一个字符串####$#%#$#,假设左边的$是现在 \(l\) 指向的地方,右边的$是现在 \(r\) 指向的地方,我么现在要求的%位置处的z函数。那么由于 \(l,r\) 就是其中一个已经求得的z函数,那么可以看出 \(l,r\) 代表的z函数值为5。

那么#####@@@@@@@@@#####@中两个#连成的字符串是完全相同的,把%加上,那么就变成了##%##@@@@@@@@@##%##@,那么现在就很直观了,##^^^@@@@@@@@@##^^^@^连成的子串也是完全相同的,也就是上面所说的。

和manacher一样,\(r\) 单调不减,总的时间复杂度是 \(O(n)\) 的。(如果你还没懂,可以造样例手玩)。

(3)应用:

Z函数主要处理的就是字符串的后缀,它可以将一个字符串的所有后缀处理出来。

(4)习题:

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

板子题,你需要处理出一个字符串的Z函数,并且需要用这个字符串与另一个字符串的所有后缀求LCP(最长公共前缀)。

直接将Z函数的求法放上去,注意其所要求的输出方式。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e7+5;
int n,m;
char s[M],t[M];
int z[M],p[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>s+1>>t+1;n=strlen(s+1),m=strlen(t+1);z[1]=m;//一般初始化z[1]=字符串长度,自己=自己
	for(int i=2,l=0,r=0;i<=m;i++)//循环从2开始
	{
		z[i]=i>r?0:min(z[i-l+1],r-i+1);//分类讨论
		while(t[1+z[i]]==t[i+z[i]]) ++z[i];//暴力添加
		if(i+z[i]-1>=r) r=i+z[i]-1,l=i;//更新r与l
	}
	for(int i=1,l=0,r=0;i<=n;i++)
	{
		p[i]=i>r?0:min(z[i-l+1],r-i+1);
		while(p[i]<m&&t[1+p[i]]==s[i+p[i]]) ++p[i];//这里的p[i]不能超过字符串的长度
		if(i+p[i]-1>=r) r=i+p[i]-1,l=i;//还是要更新
	}
	int ans=0;
	for(int i=1;i<=m;i++) ans^=i*(z[i]+1);
	cout<<ans<<"\n";ans=0;
	for(int i=1;i<=n;i++) ans^=i*(p[i]+1);
	cout<<ans<<"\n";//注意输出方式
	return 0;
}

CF432D Prefixes and Suffixes

给你一个长度为 \(n\) 的长字符串,完美子串既是它的前缀也是它的后缀,求完美子串的个数且统计这些子串在字符串中出现的次数。

非常有趣的一道题,乍一看上去既是前缀又是后缀,实际上求的就是各个boader,似乎要用kmp去做。确实这道题将 exkmp 和 kmp 结合在一起了。首先先引入两个 boader 的性质:对于任意一个字符串 \(s\) ,一个border的长度就对应一个border(比如 abcdabc 的长度为3的border当然就只能是 abc),这个还是比较显然的。

还有一个是:假设 \(s\) 长度记为 \(n\),则 \(s\) 的所有border的长度分别为:\(fail[n], fail[fail[n]], fail[fail[fail[n]]].......\)直到值为0的不算。并且这个序列的值从左往右递减(根据kmp的性质容易得出)。

所以我们只需要使用kmp将所有 boader 的长度全部处理出来,然后我们就需要用 exkmp来处理 boader 的出现次数了。对于每一个 \(z_i\) 实际上它也代表了从 \(i\) 开始,存在一个长度为 \(1\)\(z_i\) 的原串前缀,我们又知道需要找的前缀的长度。

那么我们可以借助前缀和与差分的思想,使用一个桶,将 \(t[z[i]]++\) ,然后从大向小的做一遍前缀和,我们就可以知道每一个长度的前缀的出现次数了。

最后输出的时候个数+1,因为 \(z[1]\) 没有统计哒。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<vector>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n;
char s[M];
int fail[M],z[M],t[M];
vector<int> ans;
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>s+1;n=strlen(s+1);
	for(int i=2,l=0,r=0;i<=n;i++)
	{
		z[i]=i>r?0:min(r-i+1,z[i-l+1]);
		while(s[1+z[i]]==s[i+z[i]]) ++z[i];
		if(i+z[i]-1>=r) r=i+z[i]-1,l=i;
		t[z[i]]++;//差分的思想
	}//exkmp的板子
	for(int i=n;i;i--) t[i]+=t[i+1];//将差分修改后的数组做一遍前缀和
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}
	int pos=n;
	while(pos) ans.push_back(pos),pos=fail[pos];//将所有boader的长度处理出来
	sort(ans.begin(),ans.end());
	cout<<ans.size()<<"\n";//输出boader的个数
	for(int i=0;i<ans.size();i++)
	{
		cout<<ans[i]<<" "<<t[ans[i]]+1<<"\n"; //输出boader的长度&个数+1
	}
	return 0;
}

CF526D Om Nom and Necklace

本来是一道exkmp的好题(虽然我觉得翻译有点lj),可以用kmp水过去。

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,m,fail[M];
char s[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	cin>>s+1;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}
	for(int i=1;i<=n;i++)
	{
		int len=i-fail[i],num=i/len;
		if(i%len)cout<<((num/m-num%m)>0);
		else cout<<((num/m-num%m)>=0);
	}
	return 0;
}

完结撒花★,°:.☆( ̄▽ ̄)/$:.°★

posted @ 2024-02-01 15:57  call_of_silence  阅读(76)  评论(2编辑  收藏  举报