KMP1(字符串基本概念,KMP算法和简单应用)
基础定义
字符串
\(S\):无特殊说明,字符串仅由26个小写字母\('a'-'z'\) 构成, 并用大写字母表示一个字符串。
\(|S|\):表示一个字符串的长度
\(S[i]\) : 表示字符串 \(S\) 第 \(i\) 个位置的字母,下标从 \(1\) 开始。
子串
\(S[l,r]\) : 表示字符串 \(S\) 从第 \(l\) 到第 \(r\) 个字母顺次连接而成的新字符串。
\(Pre[i]\): 表示字符串 \(S\) 的长度为 \(i\) 的前缀, \(Pre[i] = s[1,i]\)
\(Suf[i]\):表示字符串 \(S\) 的长度为 \(i\) 的后缀, \(Suf[i] = s[|S|- i + 1, |S|]\)
Border
如果字符串 \(S\) 的同长度的前缀和后缀完全相同,则称此前缀为一个Border。
特殊地,字符串本身也可以是它的 Border。(根据语境判断)
e.g. 若 \(S = bbabbab\),试求所有 Border
\(b\)
\(bbab\)
周期和循环节
对于字符串 \(S\) 和正整数 \(p\),如果有 \(S[i] = S[i - p]\) , 对于 \(p < i \le |S|\) 成立, 则称 \(p\) 为字符串 \(S\) 的一个周期。
特殊地, \(p = |S|\) 一定是 \(S\) 的周期
重要性质
\(p\) 是 \(S\) 的周期 $\iff |S| - p $ 是 \(S\) 的 Border
因此周期和Border 等价,遇到题目可以相互转化
注意: Border 不具有二分性!
Border的性质
传递性:\(S\) 的 Border 的 Border 也是 \(S\) 的 Border
求 S 的所有 Border 等价于求所有前缀的最大Border
KMP
Next数组:
next[i] 表示 pre[i] 的非平凡最大Border
next[1]=0
我们发现 \(pre[i]\) 的 Border 去掉最后一个字母就会变成 \(pre[i-1]\) 的Border
因此,求 \(pre[i]\) 的Border时,可以遍历 \(pre[i-1]\) 的所有Border,
即 \(next[i-1],next[next[i-1]],...,0\) ,检查后一个字母是否等于 \(s[i]\)
参考代码:
struct KMP{
vector<int> nxt;
void init(string s){//用来维护出nxt数组
int n=s.size();
nxt.assign(n+1,0);
s="?"+s;
for(int i=2;i<=n;i++){
nxt[i]=nxt[i-1];
while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
nxt[i]+=(s[nxt[i]+1]==s[i]);
}
}
};
例题1 字符串的问题
题意:
找出字符串 \(S\) 中满足下列条件的最长子串:
1.该子串是 \(S\) 的前缀
2.该子串是 \(S\) 的后缀
3.该子串除了开头的结尾还出现一次
思路:
转化一下题意,找到最长的在中间出现一次的Border。
朴素做法自然是枚举所有的Border,然后判断是否出现即可。
但其实,我们只用计算 \(nxt[n]\) 和 \(nxt[nxt[n]]\)。因为 \(nxt[nxt[n]]\)
说明对应的Border至少出现了四次。一定是满足题意的。
代码:
struct KMP{
vector<int> nxt;
void init(string s){
int n=s.size();
nxt.assign(n+1,0);
s="?"+s;
for(int i=2;i<=n;i++){
nxt[i]=nxt[i-1];
while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
nxt[i]+=(s[nxt[i]+1]==s[i]);
}
}
};
void Showball(){
string s;
cin>>s;
int n=s.size();
KMP kmp;
kmp.init(s);
int border=kmp.nxt[n];
for(int i=1;i<n;i++){
if(border&&kmp.nxt[i]==border){
return cout<<s.substr(0,border),void();
}
}
border=kmp.nxt[kmp.nxt[n]];
if(border==0) cout<<"Just a legend";
else cout<<s.substr(0,border);
}
例题2 字符串匹配(模板)
题意:
给出两个字符串 \(S\) 和 \(T\), 求出 \(T\) 在 \(S\) 中所有出现的位置。
思路:
枚举每个位置,遇到不同时,只需要跳到下一个Border即可。
template<class Type>
struct KMP{
vector<int> init(Type s){
int n=(int)s.size()-1;
vector<int> nxt(n+1,0);
for(int i=2;i<=n;i++){
nxt[i]=nxt[i-1];
while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
nxt[i]+=(s[nxt[i]+1]==s[i]);
}
return nxt;
}
vector<int> match(Type s,Type t){
vector<int> pos;
vector<int> nxt=init(t);
int n=(int)s.size()-1,m=(int)t.size()-1;
for(int i=1,j=0;i<=n;i++){
while(j&&s[i]!=t[j+1]) j=nxt[j];
j+=(t[j+1]==s[i]);
if(j==m){//匹配成功
j=nxt[j];
pos.push_back(i-m+1);
}
}
return pos;
}
};
KMP<string> kmp;
void Showball(){
string s,t;
cin>>s>>t;
s="?"+s;
t="?"+t;
vector<int> nxt=kmp.init(t);
vector<int> pos=kmp.match(s,t);
for(auto x:pos) cout<<x<<endl;
int n=t.size();
for(int i=1;i<n;i++) cout<<nxt[i]<<" \n"[i==n-1];
}
例题3 栗酱的数列
题意:
给你一个长度为 \(n\) 的数列 \(A\) 和一个长度为 \(m\) 的数列 \(B\)。\((m\le n)\)
求出 \(A\) 中有多少个长度为 \(m\) 的连续子序列 \(A'\) 满足 \((A'_1+B_1)\%k=(A'_2+B_2)\%k=...=(A'_m+B_m)\%k\)。
思路:
遇到这种式子,一个很经典的处理方式就是将相同类型的移项到同一边。于是有 \((A'_1-A'_2)\%k=-(B_1-B_2)\%k\)。从 \(1\) 到 \(m-1\) 都满足。那么,我们可以预处理这两个差分数组。然后问题就变成了匹配问题。我们就可以魔改一下 \(KMP\) s算法即可解决。
代码:
void Showball(){
int n,m,k;
cin>>n>>m>>k;
vector<int> a(n+1),b(m+1);
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]%=k;
}
for(int i=1;i<=m;i++){
cin>>b[i];
b[i]%=k;
}
vector<int> da(n),db(m);
for(int i=1;i<n;i++) da[i]=(a[i]-a[i+1]+k)%k;
for(int i=1;i<m;i++) db[i]=(k-(b[i]-b[i+1]+k)%k)%k;
vector<int> ans=kmp.match(da,db);
cout<<ans.size()<<endl;
}
拓展
Border的性质:
周期定理:若 \(p,q\) 均为串 \(S\) 的周期,则 \(gcd(p,q)\) 也为 \(S\) 的周期
一个串的 Border 数量是 \(O(n)\) 个,但他们组成了 \(O(logN)\) 个等差数列
KMP 的推广
扩展KMP(Z 算法)
KMP 自动机, Border树
AC自动机, 即KMP的多串模式。
Trie图, 即KMP自动机的多串模式。
Border树
定义:
对于一个字符串 \(S\), \(n = |S|\) , 它的 \(Border\) 树 共有 \(n+1\) 个节点: \(0,1,2,3,...,n\)。
\(0\) 是这课有向树的根,对于其他点,每个点的父节点是 \(nxt[i]\)
标签:nxt,int,KMP1,vector,KMP,字符串,Border,基本概念 From: https://www.cnblogs.com/showball/p/18330959性质:
1.每个前缀 \(pre[i]\) 的所有Border:节点 \(i\) 到根的链
2.哪些前缀有长度为 \(x\) 的Border: \(x\) 的子树
3.求两个前缀的公共Border 等价于求LCA