Border性质习题与证明

KMP

第一次接触 border 都是先从 KMP 开始的吧。

思想在于先对于一个串自匹配以求出 fail 指针(也就是 border) 然后就可以在匹配其他串的时候非常自然的失配转移。在此顺便给出一下 border 的定义:

Border

字符串的某个能与后缀完全匹配的真前缀(即不为原串的前缀)。

在 KMP 中我们一般关注最长的 border,然后我们 KMP 中的 fail 实际上就是存储的最长的 border 的结束的位置(因为是前缀所以可以这样存储)。

发现只有我们当前匹配的位置失配了,我们只需要不停跳 fail 直到下一位匹配即可了(或者匹配不上直接回老家)。因为当我们模式串(用于匹配的串)匹配到第 i 位,且匹配了文本串(用于被匹配的串) j 位(从某个位置开始的连续 j 个)的时候,是用模式串的一个前缀子串进行匹配的,如果此时我们将其 border 拿过来匹配,可以将其放置于原来匹配的与该前缀对应的后缀的位置,因为是 border 所以是完全重合的。

所以失配的时候跳 border 是可以匹配上的。然后我们跳最长的 border,那么我们就能匹配得尽可能长,就不会遗漏。比如说字符串 ababab,我们用 abab 去匹配它。我们很顺利的匹配了前 4 位,完成一次匹配。我们发现如果我们还有把 [3,6] 位置的字符串匹配上,我们又不能把文本串的指针往回,不然复杂度就起飞了。因此我们跳最长的 border ,模式串指针直接由于 border 的性质匹配 2 位,后面再正常匹配就是对的了。

然后我们发现 KMP 用的实际上是模式串的前缀子串的最长 border ,我们考虑怎么求这个东西。

首先第一位没有真前缀。fail[1]=0 。然后发现匹配实际上是将一个串的前缀一步一步往另一个串的前缀的后缀上匹配,这不就是匹配 border 吗?我们直接对模式串自匹配一次即可。

其实 kmp 本质上是建立了一颗 fail 树。

模板题

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e6+3;
char c[N],s[N];
int fail[N];
inline void build(char *a){
	int j=0;//第一位不匹配,满足真前缀
	int len=strlen(a+1);
	for(int i=2;i<=len;++i){
		while(j and a[j+1]!=a[i]) j=fail[j];
		if(a[j+1]==a[i]) ++j;
		fail[i]=j;
	}
}
inline void kmp(char *a,char *b){
	int j=0,len=strlen(b+1);
	int lena=strlen(a+1);
	for(int i=1;i<=len;++i){
		while(j and a[j+1]!=b[i]) j=fail[j];
		if(a[j+1]==b[i]) ++j;
		if(j==lena){
			printf("%d\n",i-j+1);
			j=fail[j];
		}
	}
}
int main(){
	filein(a);fileot(a);
	gs(c+1);gs(s+1);
	build(s);
	kmp(s,c);
	int len=strlen(s+1);
	for(int i=1;i<=len;++i){
		printf("%d ",fail[i]);
	}pc('\n');
	return 0;
}

扩展KMP

LCP

最长公共前缀。

那么扩展KMP 可以用来干嘛呢?

它可以求出 Z函数,也就是字符串的每个后缀与原串的 LCP。具体地说:z[i] 表示以 i 开头的后缀的 LCP。

其实讲得通俗一点就是对于每个点求出以它开始的子串最多能与原串开头匹配几位。

我们维护求出的右端点最右的区间 [l,r=l+z[l]+1] ,然后考虑一个要新求 Z函数的 i ,怎么利用之前原有的贡献来减少计算。

首先分为两种情况,如果 r<i ,那么显然这个区间和当前的 i 没有任何关系,从 0 开始直接暴力扩展。如果 i<=r ,那么我们知道 s[1,rl+1]=s[l,r] ,对应地,在 z[il+1]<ri+1 的情况下,也就是扩展不超过右界的前提下, z[il+1]=z[i] 。这是因为我们不能保证 r 右边的部分仍然与前缀对应。后面的部分我们暴力扩展即可。

然后我们用已经求出 Z函数的串扩展KMP 匹配,这个和自匹配求 Z函数其实是一样的不再赘述。

复杂度证明同 manacher,r 不断增大,可得复杂度为 O(n)

模板题

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=2e7+3;
char c[N],s[N];
int z[N],p[N];
inline void z_function(char *a){
	int len=strlen(a+1);
	z[1]=len;
	int l=0,r=-1;
	for(int i=2;i<=len;++i){
		if(i<=r) z[i]=std::min(z[i-l+1],r-i+1);
		while(i+z[i]<=len and a[1+z[i] ]==a[i+z[i] ])
			++z[i];
		if(i+z[i]-1>r){
			l=i;r=i+z[i]-1;
		}
	}
	ll ans=0;
	for(int i=1;i<=len;++i){
		//printf("%d ",z[i]);
		ans^=(1ll*i*(z[i]+1) );
	}
	printf("%lld\n",ans);
}
inline void exkmp(char *a,char *b){
	int len=strlen(a+1),lenb=strlen(b+1);
	int l=0,r=-1;
	for(int i=1;i<=len;++i){
		if(i<=r) p[i]=std::min(z[i-l+1],r-i+1);
		while(i+p[i]<=len and 1+p[i]<=lenb and b[1+p[i] ]==a[i+p[i] ])
			++p[i];
		if(i+p[i]-1>r){
			l=i;r=i+p[i]-1;
		}
	}
	ll ans=0;
	for(int i=1;i<=len;++i){
		//printf("%d ",p[i]);
		ans^=(1ll*i*(p[i]+1) );
	}
	printf("%lld\n",ans);
}
int main(){
	filein(a);fileot(a);
	gs(c+1);gs(s+1);
	z_function(s);
	exkmp(c,s);
	return 0;
}

Border相关性质

再次提一嘴定义:字符串的某个能与后缀完全匹配的真前缀(即不为原串的前缀)。

性质1:

border(S)=border(S)max+border(border(S)max),即一个字符串的最长 border 加上这个最长 borderborder,可以得到整个字符串所有的 border

证明:

要不我们之后简称 border 为 b 吧。(

我们假设 S[1,r]b(S)max ,若记 |S|=len ,则有 S[lenr+1,len] 为其对应后缀,那么他们的 border 除了位置不同外没有区别。因此通过相等 border 的传递,可得到 S[1,r]borderS[lenr+1,len]border 的对应后缀对应,那么等价于一对 b(S) 的前后缀。那么所有这种 border 加上其本身就可以得到所有的 b(S)

应用1:求出某个串的所有 border

操作:

我们有了性质1,只需要使用 kmp 求出 fail,然后从 n 开始一直跳 fail 就可以得到原串所有 border

提一嘴周期的概念:使得 S[i]=S[i+p] ,则 p 为串 S 的周期。容易得知,对于一个周期 pp+|b(S)|=|S| 。注意一个串能有多个周期,就和一个串能有多个 border ,那么周期与 border 相对应。

然后我们称 p|n 的周期 p 为整周期。

[POI2006] OKR-Periods of Words

题目大意:给一个字符串,要你求其所有前缀的最大周期之和。

那么我们刚刚才说了周期与 border 对应,因此我们要求最大周期,只需要找到最小 border 即可。那么我们对于每个前缀跳 fail 树即可。发现可以记忆化优化一下 fail 树,直接连到底端。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e6+3;
int n;
char c[N];
int fail[N];
int main(){
	filein(a);fileot(a);
	read(n);
	gs(c+1);
	int j=0;
	for(int i=2;i<=n;++i){
		while(j and c[j+1]!=c[i]) j=fail[j];
		if(c[j+1]==c[i]) ++j;
		fail[i]=j;
	}
	ll ans=0;
	for(int i=1;i<=n;++i){
		int j=i;
		while(fail[j]) j=fail[j];
		if(fail[i]) fail[i]=j;
		ans+=(i-j);
	}
	printf("%lld\n",ans);
	return 0;
}

[NOI2014] 动物园

题目大意:求出每个前缀的不超过 len2border 的数量。

我们先考虑暴力做法,我们直接对每个点跳 fail 树,统计有几个满足条件的 border 即可。但是显然这样做会 TLE。考虑换一种思路。

我们运用性质1容易得到对于一个 border ,比它短的加上它自身还有几个,等价于跳 fail 树去统计。那么我们得到这个东西(称为 num )以及 fail 后,我们就可以再做一次自匹配,但是每匹配完一次就往回跳 fail 直到 border 长度不超过 i2 ,这个点的 num 就是答案。注意到多匹配一位肯定只会多算不会漏算,可得正确性没有问题。均摊复杂度是 O(n) 的。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e6+3;
const int mods=1e9+7;
int n;
char c[N];
int fail[N],num[N];
int ans;
inline int mul(int x,int y){
	return 1ll*x*y%mods;
}
int main(){
	filein(a);fileot(a);
	int T;read(T);
	while(T--){
		gs(c+1);
		int len=strlen(c+1);
		int j=0;num[1]=1;
		for(int i=2;i<=len;++i){
			while(j and c[j+1]!=c[i]) j=fail[j];
			if(c[j+1]==c[i]) ++j;
			fail[i]=j;num[i]=num[j]+1;
			//printf("[%d] ",num[i]);
			//性质1
		}//pc('\n');
		j=0;ans=1;
		for(int i=2;i<=len;++i){
			while(j and c[j+1]!=c[i]) j=fail[j];
			if(c[j+1]==c[i]) ++j;
			while(2*j>i) j=fail[j];
			ans=mul(ans,num[j]+1);
			//printf("(%d) ",num[j]);
		}//pc('\n');
		printf("%d\n",ans);
	}
	return 0;
}

【模板】失配树

题目大意:多次询问,求两个前缀的最长公共前缀的长度。

容易发现,两者一同跳 fail 树直到出现公共部分,这不就是 fail 树上的 LCA 吗?然后根据 border 定义把 LCA 为两前缀之中的一个的情况特殊处理,再跳一步即可。

注意题面,不是求公共前缀有几个。(我也不知道我怎么看错的)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e6+3;
char s[N];
int n,fail[N];
int fa[N][20+3],dep[N];
inline int LCA(int x,int y){
	if(dep[x]<dep[y]) std::swap(x,y);
	for(int i=20;i>=0;--i){
		if(dep[fa[x][i] ]>=dep[y]){
			x=fa[x][i];
		}
		if(x==y) return x;
	}
	for(int i=20;i>=0;--i){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return fa[x][0];
}
int main(){
	filein(a);fileot(a);
	gs(s+1);
	n=strlen(s+1);
	int j=0;
	dep[1]=1;
	for(int i=2;i<=n;++i){
		while(j and s[j+1]!=s[i]) j=fail[j];
		if(s[j+1]==s[i]) ++j;
		fa[i][0]=fail[i]=j;
		dep[i]=dep[j]+1;
	}
	for(int i=0;i<20;++i){
		for(j=1;j<=n;++j){
			fa[j][i+1]=fa[fa[j][i] ][i];
		}
	}
	int Q;read(Q);
	while(Q--){
		int a,b;
		read(a,b);
		int f=LCA(a,b);
		if(f==a or f==b) f=fail[f];
		printf("%d\n",f);
	}
	return 0;
}

[POI2005]SZA-Template

题目大意:给一个字符串,问最短用一条多长的子串,能够将原串完全覆盖(不要求精准覆盖)。比如 aaa 可以用 a 完全覆盖。

发现这个子串必定是原串的 border ,在失配树上它们表现为一条链。然后我们证一个结论:只要在 u 的失配树子树中的点拿出来排序,相邻的最大间隔不超过 u ,那么 u 可以完全覆盖(前提还是 u 是原串的 border)。

这个不难证明,首先由于这些子树中的点都满足 u 是其 border ,那么可以把 u 从后往前接上,如果最大间隙不超过 u ,也就说明它的覆盖之间没有间隙,那么可以完全覆盖。

那么这个东西我们维护只删除的链表即可,我们在失配树上dfs,每次删掉父节点下除去原串 border 链所在子树的其他子树(父节点也要删),然后直接搞就没了。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=5e5+3;
const int inf=1e9;
char c[N];
int fail[N],n;
int sta[N],top;
std::vector<int>G[N];
bool mark[N];
int mx=1;
struct list{
	int l,r;
}p[N];
inline void del(int x){
	mx=std::max(mx,p[x].r-p[x].l);
	p[p[x].r].l=p[x].l;
	p[p[x].l].r=p[x].r;
}
void dfs_del(int u){
	del(u);
	for(auto v:G[u]){
		dfs_del(v);
	}
}
int ans=0;
void dfs(int u){
	if(mx<=u){
		printf("%d\n",u);
		exit(0);
	}
	del(u);
	for(auto v:G[u]){
		if(!mark[v])
			dfs_del(v);
	}
	for(auto v:G[u]){
		if(mark[v]){
			dfs(v);
			return;
		}
	}
}
int main(){
	filein(a);fileot(a);
	gs(c+1);
	n=strlen(c+1);
	G[0].push_back(1);
	for(int i=2,j=0;i<=n;++i){
		while(j and c[j+1]!=c[i]) j=fail[j];
		if(c[j+1]==c[i]) ++j;
		fail[i]=j;
		G[j].push_back(i);
	}
	for(int i=n;i;i=fail[i]) mark[i]=1;
	for(int i=0;i<=n;++i){
		p[i].l=i-1;
		p[i].r=i+1;
	}
	p[n].r=0;p[0].l=n;
	dfs(0);
	return 0;
}

性质2:

WPL(Weak Periodicity Lemma):对于周期 p,q ,如果 p+qn ,那么 gcd(p,q) 也是周期。

证明:

我们假设 p<q,那么对于每个 i ,当 nq+pi>p 时,有 s[i]=s[ip]=s[ip+q] ;当 i<=p ,有 s[i]=s[i+q]=s[i+qp],那么对于每个 i ,有 s[i]=s[i+pq],那么可以得出结论 pq 也是一个周期。

那么就知道了 p+(qp)n ,一直搞下去等价于 gcd 辗转相减。那么其实对于所有能够辗转相减得到的都是周期,不一定非得 gcd(p,q)

Q.E.D.

性质3:

对于两个字符串 s,tst 的前缀,t 有周期 as 有周期 b, 满足 a|s|b|a ,那么有 bt 的周期。

证明:

由于 b|aa|s| ,所以可知 a 也为 s 的周期。又由于 st 的前缀,所以 s 完全覆盖 t 的前缀 a,那么 t 可以看做是有多个 s 的前缀 a 拼接而成。而显然对于 s 的前缀 a,其有周期 b ,而 t 又有周期 a ,那么 b 也为 t 的周期。

性质4:

对于两个字符串 s,t ,有 2|s||t|,则 st 上的匹配位置是一个等差数列。

证明:

不妨先将 t 未与 s 匹配的部分先剔除,然后设第一次匹配与第二次匹配位置相差 p,之后某次匹配与第一次匹配相差 q

我们可以得到 gcd(p,q) 位置也是一个匹配位置。由于 p 为第一次匹配位置,也即最小的匹配位置,显然有 gcd(p,q)=p ,那么可知 p|q 。显然那么 t 有周期 p ,每次匹配位置都相差 p ,因此在匹配位置上是公差为 p 的等差数列。

Q.E.D.

性质5:

对于一个字符串 s,其 n2border 构成一个等差数列。

证明:

设最长 bordernp ,某个 bordernq ,且 p,qn2

那么可以知道,ngcd(p,q) 也是 border ,然后由 np 为最长 border ,可知 gcd(p,q)p ,即 p|q。那么其为公差为 p 的等差数列。

Q.E.D.

性质6:

一个字符串 sborder 按照长度排序后可划分为 O(logn) 个等差数列。

证明:

先对于串对半分成两部分。然后可以发现后半段中的 border 形成等差数列,而前半段形成一个小的子问题。后用性质4即可证明。

Q.E.D.

性质7:

字符串 s 的公差 dborder 等差数列的总大小为 O(nd) 的。

证明:

我们顺着性质6的证明来。对于一个区间 [2i1,2i) ,公差 p 满足 p2i2i1 。对于一个组内其大小为 2i2i1p,那么公差 dborder 等差数列的总大小就是:

pid2i2i1pi

我们证明其上界,pi 我们只少取不多取,取到 d2i2i1 只多取不少取,取到 n 。那么总大小上界 nd ,即总大小 O(nd)

Q.E.D.

kmp优化小技巧

border 长度大于总长的一半,字符串 S 可写作 BAk , 此时我们直接跳到 B 可以使得复杂度变为 O(nlogn) 而非均摊 O(n) ,可以用来做 trie 树的 kmp

	for(int i=2,j=0;i<=lenc;++i){
	    while(j and s[j+1]!=s[i]){
  		if(j+1<i and fail[j+1]*2>(j+1) ) j=(j-1)%(j-fail[j])+1;
  		else j=fail[j];
  	}
		if(c[j+1]==c[i]) ++j;
		fail[i]=j;
	}

模板

应用

posted @   cbdsopa  阅读(419)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示