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,r-l+1]=s[l,r]\) ,对应地,在 \(z[i-l+1]<r-i+1\) 的情况下,也就是扩展不超过右界的前提下, \(z[i-l+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\) 加上这个最长 \(border\) 的 \(border\),可以得到整个字符串所有的 \(border\)。
证明:
要不我们之后简称 \(border\) 为 b 吧。(
我们假设 \(S[1,r]\) 为 \(b(S)_{max}\) ,若记 \(\lvert S\rvert=len\) ,则有 \(S[len-r+1,len]\) 为其对应后缀,那么他们的 \(border\) 除了位置不同外没有区别。因此通过相等 \(border\) 的传递,可得到 \(S[1,r]\) 的 \(border\) 与 \(S[len-r+1,len]\) 的 \(border\) 的对应后缀对应,那么等价于一对 \(b(S)\) 的前后缀。那么所有这种 \(border\) 加上其本身就可以得到所有的 \(b(S)\) 。
应用1:求出某个串的所有 \(border\)。
操作:
我们有了性质1,只需要使用 kmp 求出 \(fail\),然后从 \(n\) 开始一直跳 \(fail\) 就可以得到原串所有 \(border\)。
提一嘴周期的概念:使得 \(S[i]=S[i+p]\) ,则 \(p\) 为串 \(S\) 的周期。容易得知,对于一个周期 \(p\),\(p+\lvert b(S)\rvert=\lvert S\rvert\) 。注意一个串能有多个周期,就和一个串能有多个 \(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;
}
题目大意:求出每个前缀的不超过 \(\lfloor\frac{len}{2}\rfloor\) 的 \(border\) 的数量。
我们先考虑暴力做法,我们直接对每个点跳 \(fail\) 树,统计有几个满足条件的 \(border\) 即可。但是显然这样做会 TLE。考虑换一种思路。
我们运用性质1容易得到对于一个 \(border\) ,比它短的加上它自身还有几个,等价于跳 \(fail\) 树去统计。那么我们得到这个东西(称为 \(num\) )以及 \(fail\) 后,我们就可以再做一次自匹配,但是每匹配完一次就往回跳 \(fail\) 直到 \(border\) 长度不超过 \(\lfloor\frac{i}{2}\rfloor\) ,这个点的 \(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;
}
题目大意:给一个字符串,问最短用一条多长的子串,能够将原串完全覆盖(不要求精准覆盖)。比如 \(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+q\le n\) ,那么 \(gcd(p,q)\) 也是周期。
证明:
我们假设 \(p<q\),那么对于每个 \(i\) ,当 \(n-q+p\ge i>p\) 时,有 \(s[i]=s[i-p]=s[i-p+q]\) ;当 \(i<=p\) ,有 \(s[i]=s[i+q]=s[i+q-p]\),那么对于每个 \(i\) ,有 \(s[i]=s[i+p-q]\),那么可以得出结论 \(p-q\) 也是一个周期。
那么就知道了 \(p+(q-p)\le n\) ,一直搞下去等价于 \(gcd\) 辗转相减。那么其实对于所有能够辗转相减得到的都是周期,不一定非得 \(gcd(p,q)\)。
Q.E.D.
性质3:
对于两个字符串 \(s,t\) ,\(s\) 为 \(t\) 的前缀,\(t\) 有周期 \(a\) ,\(s\) 有周期 \(b\), 满足 \(a\le|s|\) 且 \(b|a\) ,那么有 \(b\) 为 \(t\) 的周期。
证明:
由于 \(b|a\) 且 \(a\le|s|\) ,所以可知 \(a\) 也为 \(s\) 的周期。又由于 \(s\) 为 \(t\) 的前缀,所以 \(s\) 完全覆盖 \(t\) 的前缀 \(a\),那么 \(t\) 可以看做是有多个 \(s\) 的前缀 \(a\) 拼接而成。而显然对于 \(s\) 的前缀 \(a\),其有周期 \(b\) ,而 \(t\) 又有周期 \(a\) ,那么 \(b\) 也为 \(t\) 的周期。
性质4:
对于两个字符串 \(s,t\) ,有 \(2|s|\ge |t|\),则 \(s\) 在 \(t\) 上的匹配位置是一个等差数列。
证明:
不妨先将 \(t\) 未与 \(s\) 匹配的部分先剔除,然后设第一次匹配与第二次匹配位置相差 \(p\),之后某次匹配与第一次匹配相差 \(q\) 。
我们可以得到 \(gcd(p,q)\) 位置也是一个匹配位置。由于 \(p\) 为第一次匹配位置,也即最小的匹配位置,显然有 \(gcd(p,q)=p\) ,那么可知 \(p|q\) 。显然那么 \(t\) 有周期 \(p\) ,每次匹配位置都相差 \(p\) ,因此在匹配位置上是公差为 \(p\) 的等差数列。
Q.E.D.
性质5:
对于一个字符串 \(s\),其 \(\ge \frac{n}{2}\) 的 \(border\) 构成一个等差数列。
证明:
设最长 \(border\) 为 \(n-p\) ,某个 \(border\) 为 \(n-q\) ,且 \(p,q\le \frac{n}{2}\)。
那么可以知道,\(n-gcd(p,q)\) 也是 \(border\) ,然后由 \(n-p\) 为最长 \(border\) ,可知 \(gcd(p,q)\ge p\) ,即 \(p|q\)。那么其为公差为 \(p\) 的等差数列。
Q.E.D.
性质6:
一个字符串 \(s\) 的 \(border\) 按照长度排序后可划分为 \(O(\log n)\) 个等差数列。
证明:
先对于串对半分成两部分。然后可以发现后半段中的 \(border\) 形成等差数列,而前半段和后半段中的第一个 \(border\) 形成一个小的子问题。只需考虑后半段的第一个 \(border\) 的位置即可。
我们设后半段公差为 \(p\) 。\(p\le \frac{n}{4}\) 时,由最大 \(border\ n-p\) 连续减去 \(p\),可得后半段的第一个 \(border\) \(\le \frac{3}{4}n\) ; \(p>\frac{n}{4}\) 时,\(n-p\) 就已经满足 \(\le \frac{3}{4}n\),更不用说后半段的第一个 \(border\)。
由性质1,剩下的 \(border\) 都是这个后半段最小的 \(border\) 的 \(border\),可知我们每次操作最少可以缩小到原范围的 \(\frac{3}{4}\) ,最多 \(\frac{1}{2}\) 。发现证不了它是 \(O(\log n)\) 的。
好吧,似乎换一种思路会更好。
我们将原串的 \(border\) 长度倍增地分为形如 \([2^{i-1},2^i)\) 的 \(O(\log n)\) 部分。
我们考虑区间 \([2^{i-1},2^i)\) ,其最大的 \(border\) 为 \(2^i-p\),由性质1可知其他区间内的 \(border\) 都是这个最大 \(border\) 的 \(border\) 。然后由于性质3可以得知这个区间内的所有 \(border\) 构成公差为 \(p\) 的等差数列。
那么总共分了 \(O(\log n)\) 个区间,可以得到 \(O(\log n)\) 个等差数列。即使算上相邻两个区间的最大和最小 \(border\) 可能构成独一的等差数列的情况,也不过是多一份 \(2\) 的常数,总体依旧是 \(O(\log n)\)。其他情况只会少不会多。(但是实际上边界的 \(border\) 只会融入到两边的等差数列里面去)
Q.E.D.
性质7:
字符串 \(s\) 的公差 \(\ge d\) 的 \(border\) 等差数列的总大小为 \(O(\frac{n}{d})\) 的。
证明:
我们顺着性质6的证明来。对于一个区间 \([2^{i-1},2^i)\) ,公差 \(p\) 满足 \(p\le 2^i-2^{i-1}\) 。对于一个组内其大小为 \(\frac{2^i-2^{i-1}}{p}\),那么公差 \(\ge d\) 的 \(border\) 等差数列的总大小就是:
\]
我们证明其上界,\(p_i\) 我们只少取不多取,取到 \(d\);\(\sum 2^i-2^{i-1}\) 只多取不少取,取到 \(n\) 。那么总大小上界 \(\frac{n}{d}\) ,即总大小 \(O(\frac{n}{d})\)。
Q.E.D.
Border性质习题与证明的更多相关文章
- Border Theory
持续更新中!!!更个屁,无线停更! 前言: KMP 学傻了,看 skyh 说啥 border 树,跑来学 border 理论 洛谷云剪切板:https://www.luogu.com.cn/paste ...
- WPF中的常用布局 栈的实现 一个关于素数的神奇性质 C# defualt关键字默认值用法 接口通俗理解 C# Json序列化和反序列化 ASP.NET CORE系列【五】webapi整理以及RESTful风格化
WPF中的常用布局 一 写在开头1.1 写在开头微软是一家伟大的公司.评价一门技术的好坏得看具体的需求,没有哪门技术是面面俱到地好,应该抛弃对微软和微软的技术的偏见. 1.2 本文内容本文主要内容 ...
- 【poj2478-Farey Sequence】递推求欧拉函数-欧拉函数的几个性质和推论
http://poj.org/problem?id=2478 题意:给定一个数x,求<=x的数的欧拉函数值的和.(x<=10^6) 题解:数据范围比较大,像poj1248一样的做法是不可行 ...
- 统计学习导论:基于R应用——第三章习题
第三章习题 部分证明题未给出答案 1. 表3.4中,零假设是指三种形式的广告对TV的销量没什么影响.而电视广告和收音机广告的P值小说明,原假设是错的,也就是电视广告和收音机广告均对TV的销量有影响:报 ...
- HDU6438 Buy and Resell 解题报告(一个有趣的贪心问题的严格证明)
写在前面 此题是一个很容易想到的贪心题目,但是正确性的证明是非常复杂的.然而,目前网上所有题解并未给出本题贪心算法的任何正确性证明,全部仅停留在描述出一个贪心算法.本着对算法与计算机科学的热爱(逃), ...
- 【border树】【P2375】动物园
Description 给定一个字符串 \(S\),对每个前缀求长度不超过该前缀一半的公共前后缀个数. 共有 \(T\) 组数据,每组数据的输出是 \(O(1)\) 的. Limitations \( ...
- 2020-BUAA OO-面向对象设计与构造-HW11中对ageVar采用缓存优化的等价性证明(包括溢出情况)
HW11中对ageVar采用缓存优化的等价性证明(包括溢出情况) 概要 我们知道,第三次作业里age上限变为2000,而如果缓存年龄的平方和,2000*2000*800 > 2147483647 ...
- [nfls338]基本字典子串
1.前置知识 以下数字未特殊说明,取值范围均与$N$取交 以下字符串未特殊说明,下标均从1开始,且均为非空串,复杂度中的$n$指字符串长度 周期和border 对于非空集合$S$,定义$\ ...
- AT2651 [ARC077D] SS
定义 \(nxt_i\) 表示在字符串 \(S\) 中以 \(i\) 结尾的最长 \(border\). 引理一:若 \(n - nxt_n \mid n\) 则 \(S_{1 \sim n - nx ...
随机推荐
- React 可视化开发工具 shadow-widget 的非可视开发方法
Shadow Widget 提倡在可视设计器中开发用户界面,输出转义标签,而非 JSX.许多童鞋可能不知道 SW 同样支持用 JSX 设计界面,开发体验比原生 React 编程好出很多,本文就介绍这方 ...
- 快如闪电,触控优先。新一代的纯前端控件集 WijmoJS发布新版本了
全球最大的控件提供商葡萄城宣布,新一代纯前端控件 WijmoJS 发布2018 v1 版本,进一步增强产品功能,并支持在 Npm 上的安装和发布,极大的提升了产品的易用性. WijmoJS 是用 Ty ...
- CCF201604-2俄罗斯方块
问题描述 俄罗斯方块是俄罗斯人阿列克谢·帕基特诺夫发明的一款休闲游戏. 游戏在一个15行10列的方格图上进行,方格图上的每一个格子可能已经放置了方块,或者没有放置方块.每一轮,都会有一个新的由4个小方 ...
- Docker 核心知识回顾
Docker 核心知识回顾 最近公司为了提高项目治理能力.提升开发效率,将之前的CICD项目扩展成devops进行项目管理.开发人员需要对自己的负责的项目进行流水线的部署,包括写Dockerfile ...
- Servlet学习记录
个人认为servlet属于一种控制程序,可以处理浏览器的请求并做出对应的回应.我们经常使用的是让一个类去继承HttpServlet,然后在doget或者dopost里面写东西. 目前我个人常在doge ...
- 简简单单用一下 Hbase
一.Hbase 介绍 https://hbase.apache.org/book.html#_preface https://blogs.apache.org/hbase/ https://resea ...
- python基础练习题(题目 判断101-200之间有多少个素数,并输出所有素数。)
day7 --------------------------------------------------------------- 实例012:100到200的素数 题目 判断101-200之间 ...
- 『现学现忘』Git基础 — 11、配置Git用户签名的方式
目录 1.配置Git签名 (1)语法 (2)配置系统用户签名 (3)配置全局用户签名 (4)配置本地用户签名 2.查看三个配置文件的用户签名 (1)语法 (2)查看项目/仓库级别的配置文件信息(loc ...
- OrchardCore Headless建站拾遗
书接上回,OrchardCore的基本设置写了,但是有一说一,这个东西还是挺复杂的,如果需要构建一个简单的企业网站,还需要干点别的活. 本文考虑在尽量少编程的基础上,完成一个Headless网站的设置 ...
- python3 Softmax函数
Softmax函数公式 Softmax的作用简单的说就计算一组数值中每个值的占比 import torch import torch.nn.functional as F # 原始数据tensor y ...