KMP
作者:互联网
KMP
字符串基本概念
字符串
S:无特殊说明,字符串仅由26个小写字母'a'-'z',并用大写字母表示一个字符串 S="abcd"
|S|:表示一个字符串的长度 |S|=4
S[i]:表示字符串S第i个位置的字母,下标从1开始(一般在字符串最前面加上一个空格) S[1]='a'
子串
S[l,r]:表示字符串S从第l到第r个字母顺次连接而成的新字符串 S[2,3]="bc"
Prefixs[i] :表示字符串S的长度为i的前缀,Prefixs[i] = S[1,i] Prefix[2]=S[1,2]="ab"
Suffixs[i]:表示字符串S的长度为i的后缀,Suffixs[i] = S[|S|-i+1,|S|] Suffix[3]=s[2,4]="bcd"
注意,如果语境中只存在一个字符串,则可以简写成Prefix[i]和Suffix[i]
Border
如果字符串S 的同长度的前缀和后缀完全相同,即Prefix[i] = Suffix[i],
则称此前缀(后缀)为一个Border(根据语境,有时Border 也指长度)。
特殊地,字符串本身也可以是它的Border,具体是不是根据语境判断。
e.g. 若S="bbabbab",试求所有Border
"b" 和 "bbab" 也可以说1和4是border
周期和循环节
对于字符串S 和正整数p,如果有S[i] = S[i − p],对于p < i ≤ |S| 成立,则称p 为字符串S 的一个周期。
特殊地,p = |S| 一定是S 的周期
e.g. s="bbabbab" p=3 "bba" "bba" "b" 或者 p=6 "bbabba" "b" 或者p=7 "bbabbab" 共有3个循环周期
循环周期可以要求循环单元不完整出现,例如上面例子里面最后一个循环单元没有完整出现
若字符串S 的周期p 满足p | |S|,则称p 为S 的一个循环节
判断P是否能整除|S|
循环节要求所有循环单元都必须要完整出现
特殊地,p = |S| 一定是S 的循环节
e.g. S="bbabbab" 循环节只有本身"bbabbab"
S="bbabbabba" 循环节有"bba" "bbabbabba"
Border vs 周期
p 是S 的周期⇔ |S| − p 是S 的Border
证明.
p 为S 的周期⇔ S[i − p] = S[i]
q 为S 的Border ⇔ S[1, q] = S[|S| − q + 1, |S|] ⇔
S[1] = S[|S| − q + 1], S[2] = S[|S| − q + 2], . . . , S[q] = S[|S|]
S[i] = S[i+|S|-q] ⇔ |S|-q=p
易得:p + q = |S|
因此,字符串的周期性质等价于Border 的性质,
求周期也等价于求Border
警告:Border 不具有二分性。
Border的Naive 求法
暴力
枚举1 ≤ i ≤ |S|,暴力验证是否有Preffix[i] == Suffix[i] 。
复杂度O(N2)
优雅的暴力
使用Hash 验证Prefix[i] == Suffix[i]
复杂度O(N),常数很大,容易构造Hash 冲突
Border的性质
传递性
S 的Border 的Border 也是S 的Border
可以画图表示
证明.
设p 为S 的Border,则有Preffixs[p] == SuffixS[p],即
S[1, p] == S[|S| − p + 1, |S|]
设q 为S[1, p] 的Border,则有PrefixS[1,p][q] == SuffixS[1,p][q],即
S[1, q] == S[p − q + 1, p],进而S[1, q] == S[|S| − q + 1, |S|],因此q 也是S 的Border。
"bbabbab" border:"bbab" bbab border:"b"
所以 , 求S 的所有Border ⇔ 求所有前缀的最大Border
令p为S的最大border,那么S的其他border也是p的border,
要求S的所有border,就是先求S 的最大border,再对这个最大border求最大border,直到除了本身以外没有其他border(非平凡),就找到了S 的所有border
KMP算法和简单应用
KMP用来求每个前缀的最大border
Next数组
next[i] = Preffix[i] 的非平凡的最大Border (非平凡就是去掉本身)
next数组表示i这个前缀的除了本身之外的最大border
next[1] = 0 (长度为1的字符串没有非本身的border)
考虑Prefix[i] 的所有(长度大于1 的)Border,去掉最后一个字母,就会变成Prefix[i − 1] 的Border。
蓝色部分代表字符串的border,那么我们把前缀的最后一个字符扣掉,再把后缀的最后一个字符扣掉,那么红色实心部分也应该是相等的
所以求长度为i的border等价于求长度为i-1的border,我们要求有没有长度为i的border的时候,其实就只要判断i-1的border+1后是否相等就行
Prefix[i]的border长度-1 = Prefix[i-1]的border长度 这是必要性 然后我们通过Prefix[i-1]的border+1去判断能否得到prefix[i]的border,从而证明充分性
这里只能由Prefix[i]的border推Prefix[i-1]的border,不能反过来推
就是说,我判断Prefix[i]的border的时候,我看一下能不能由prefix[i-1]的最长border next[i-1]往后拓展一个字符得到,如果不能的话我就去判断能不能由next[next[i-1]]拓展一个字符得到,直到最后next[]=0为止
因此求next[i] 的时候,可以遍历Prefix[i − 1] 的所有Border,即next[i − 1], next[next[i − 1]], . . . , 0,检查后一个字符是否等于S[i]。
这看着也太O(N2) 了??
e.g. S="bbabbab"
next[1]=0 长度为1的前缀有一个长度为0的border
next[2] 就是求"bb"的border,通过next[1]+1=1,在第一个border的基础上向后拓展一个字符,判断长度为1的是不是prefix[2]的border,那么b是bb的border 所以 next[2]=1
next[3] 将next[2]+1=2,在next[1]的基础上向后拓展一位,b->bb , b->ba , 看长度为2的字符串是不是长度为3的前缀的border "bb"!="ba" ,再去看next[1]=0 再去检查长度为0+1的border是不是长度为3的前缀的border "b"!="a" ,所以next[3]=0
next[4] next[3]+1=1,判断b是不是长度为4的前缀的border,就是求"bbab"的border,next[4]=1
next[5] next[4]+1=2,判断长度为2的字符是不是长度为5的border,b->bb,b->bb,bb是bbabb的border,next[5]=next[4]+1 "bbabb" 前面一个的border是b 也就是bbab b 在前缀b和后缀b加上一个字符判断是不是相等的 bb == bb next[5]=2
next[6] next[5]+1=3 bb->bba ,bb->bba ,bb是bbabba的border,所以next[6]=next[5]+1=3
next[7] next[7]=next[6]+1=4 bba->bbab ,bba->bbab,bbab是bbabbab的border,所以next[7]=next[6]+1=4
原理就是判断i-1的border拓展一位能不能变成i的border,就是说看i-1的前缀 的前缀和后缀都往后拓展一位是不是还相同,相同就将i-1 去+1,得到答案。如果不相同就看在前面一次也就是得到i-1的答案的那个border是否能够再拓展一位
(也就是说一个字符串的次大border一定是最大border的border)
最后的border就是next[|S|]
复杂度分析
考虑使用势能分析进行讨论:
如果next[i] = next[i − 1] + 1,则势能会增加1
否则势能会先减少到某个next[j],然后有next[i] = next[j]+1,势能也会增加1,在寻找next[j] 的过程中,势能会减少,每次至少减少1。
还有一种情况,next[i] = 0,势能清空,且不会增加。
综上,势能总量为O(N),因此整体的复杂度也是O(N),常数为2 左右(很小)。空间复杂度也为O(N)。
例题1 NC15165 字符串的问题
字符串S 长度不超过106,求一个最长的子串T,满足:
T 为S 的前缀。
T 为S 的后缀。
T 在S 中至少出现3 次。
T 为S 的前缀 + T 为S 的后缀 = T是S的border
T还要在其他位置也出现一遍,所以T还是S的某个前缀的border(通过长度判断)
那么就是看最大border,即next[n]是否在前面出现过,出现过就是这个next[n]
如果没有那么就是次大border,即next[next[n]],次大border最起码出现过四次,在前缀里面出现过两次,在后缀里面出现过两次
首先用KMP 求出S 的所有Border,答案为next[n] 或者next[next[n]]。(次大border最少会出现4次)
border要出现至少3次,那么nxt[n]就至少要能够匹配两次,那就直接输出border为nxt[n]的前缀就可以了
那如果nxt[n]和nxt[nxt[n]]都为0,就输出无解
#include <bits/stdc++.h>
//#define LOCAL
using namespace std;
typedef long long ll;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
const double pi=acos(-1.0);
const int INF=1000000000;
const int maxn=1e6+5;
int nxt[maxn];
int main()
{
IOS;
#ifdef LOCAL
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
string ss;
cin>>ss;
int lenss=ss.length();
ss=" "+ss;
for (int i=2;i<=lenss;i++){
nxt[i] = nxt[i-1];
while (nxt[i]&&ss[i]!=ss[nxt[i]+1]) nxt[i] = nxt[nxt[i]];
nxt[i]+=(ss[i]==ss[nxt[i]+1]);
}
int p=0;
for(int i=1;i<=lenss;i++){
if(nxt[lenss]==nxt[i]) p++;
}
if(p>=2&&nxt[lenss]){
for(int i=1;i<=nxt[lenss];i++)
cout<<ss[i];
}else if(nxt[nxt[lenss]]){
for(int i=1;i<=nxt[nxt[lenss]];i++){
cout<<ss[i];
}
}else cout<<"Just a legend";
return 0;
}
例题2 NC16638 carpet
有一个n ∗ m 的字符串二维矩阵A(0 < n ∗ m ≤ 1000, 000)。
求一个最小的子矩阵B,使得:将矩阵B 横向纵向无限复制之后,A 是一个子矩阵。
题意等价于求A 的最小二维循环周期。二维循环周期需要对两个维度分别求。方法是完全对称的。矩阵的横向循环周期,必须同时是矩阵每一行的循环周期。因此对每一行分别求循环周期(KMP),然后求最小公共周期即可。
求横向的最小循环周期和纵向的最小循环周期
KMP可以求border,然后|S|-border就是周期
B是A的子矩阵,然后将B无限复制,A就变成了B 的子矩阵,那么我们可以猜测,B是A的循环周期,那么对于横向和纵向都是一样的原理,
所以我们只需要求出A的各行的循环周期,然后再去求公共周期就可以了,纵向和横向一样
然后p为周期,那么|S|-p为border,所以可以通过求border来求周期
例题3 P3375 KMP字符串匹配
给出两个字符串S,和T,求出T 在S 中所有出现位置。
例如:S = abababc, T = aba,则T 在S 的所有出现位置为1 和3。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+7;
int nxt[maxn];
void init(string s){
int len = s.length();
for(int i=2;i<=len;i++){
nxt[i] = nxt[i-1];
while(nxt[i] && s[i] != s[nxt[i]+1]) nxt[i] = nxt[nxt[i]];
nxt[i] += (s[i] == s[nxt[i]+1]);
}
}
int main(){
ios::sync_with_stdio(false);
string str,ss;cin >> str>>ss;
str = " " + str;
ss = " " + ss;
init(ss);
int lenstr = str.length();
int lenss = ss.length();
int tmp = 0;
for(int i = 1;i < lenstr;i++){
while(tmp && ss[tmp+1] != str[i]) tmp = nxt[tmp];
if(ss[tmp+1] == str[i]) tmp++;
if(tmp == lenss-1){
cout<<i-tmp+1<<"\n";
tmp= nxt[tmp];
}
}
for(int i = 1;i < lenss;i++) cout<<nxt[i]<<" ";
}
//这里的字符串是先在前面加空格再去求长度的,要注意
//真的没想明白哪里卡常。。。
字符串匹配
Naive 的匹配
枚举起始位置,然后暴力匹配。复杂度O(N2)
优雅的暴力
枚举起始位置,然后用Hash 检查。复杂度O(N),常数极大。字符集很大时的处理比较繁琐。
KMP 匹配
KMP 充分利用前缀匹配的有效信息,即next 数组(Border 的性质),进行快速转移。
KMP 匹配
假设在暴力匹配的过程中发生了如下情况:
T1为要搜索的串,S代表被搜索的串
绿色部分表示匹配成功,空格表示匹配失败
由于红蓝方块位置的字符不匹配,因此需要合理向右移动T 字符串,在成功匹配了绿色方块位置的字符之后,才可以继续向后匹配:
就是移动绿色的条带,使得方块的位置能够匹配上,后面的位置才有可能继续匹配
此时可以清晰的看到,T2 绿条部分,恰好是T1 绿条部分的Border。
所以匹配失败位置的后缀和前缀相同,也就是说要把border卡在前一次匹配未成功的位置,也就是每次要往后平移前缀为匹配失败的位置的所有border个单位,
要跳border链去判断
也就是说,当遇到匹配失败的字符时,只需要考虑Border 所有的长度即可,非Border 长度一定不会匹配的更“远”。
KMP 匹配的复杂度分析
使用KMP 进行字符串匹配时,利用势能分析,不难看出总势能为|S|,
再加上预处理T 的next 数组,复杂度为O(|S| + |T|)。
例题4 NC14694 栗酱的数列
给出两个正整数数组A 和B,长度分别为n ≤ m ≤ 2 · 105,求A 有多少个长度为m 的区间A′ 满足:
(A′[1] + B[1])%k = (A′[2] + B[2])%k = . . . (A′[m] + B[m])%k
题解
要求满足的条件为:
(1) (A′[1] + B[1])%k = (A′[2] + B[2])%k
(2) (A′[2] + B[2])%k = (A′[3] + B[3])%k
. . .
(m) (A′[m − 1] + B[m − 1])%k = (A′[m] + B[m])%k
移项得到:
(1) (A′[1] − A′[2])%k = −(B[1] − B[2])%k
(2) (A′[2] − A′[3])%k = −(B[2] − B[3])%k
. . .
(m) (A′[m − 1] − A′[m])%k = −(B[m − 1] − B[m])%k
对A'数组求差分得到Diff A,对B'数组也求差分得到Diff B,再变成- Diff B
因此答案等于−DiffB 数组在DiffA 数组中的出现次数。
进而问题转化为字符串匹配问题,可以使用KMP 解决。
拓展
Border 的性质
周期定理:若p, q 均为串S 的周期,则(p, q) 也为S 的周期。
S[i] = S[i+p] = S[i+q]
S[j] = S[j+q-p] (q>p) T=q-p (辗转相除法 最终可以得到gcd(q.p) )
分为强周期定理和弱周期定理
一个串的Border 数量是O(N) 个,但他们组成了O(logN) 个等差数列。
e.g. 对于一个全a串,他的border数量为n,但是组成的等差数列为1
KMP 的推广
拓展KMP(a.k.a Z 算法)
KMP 自动机,Border 树
AC 自动机,即KMP 的多串模式。
Trie 图,即KMP 自动机的多串模式。
Border树
对于一个字符串S,n=|S|,他的border树,也叫next树,共有n+1个节点:0,1...n(0的地方标记为空集)
0是这颗有向树的根,对于其他的每个点1-n,父节点为next[i]
就是说从i开始,父节点为next[i],父节点的父节点为next[next[i]],一直到根节点(空集)
性质:
每个前缀prefix[i]的所有border:节点i到根的链
哪些前缀有长度为x的border:x的子树
求两个前缀的公共border等价于求LCA
B站KMP算法易懂版
KMP:快速从主串中找到想要的模式串
当我们找到模式串和主串不一样的地方,指针就停止右移比较并停下来。这个时候指针左边的主串部分和模式串部分都是一样的,并且模式串中有公共前后缀(border)
然后直接向右移动模式串,使得模式串的prefix和指针左边的主串的suffix位置重合,现在指针左边的串是上下匹配的
如果模式串有多个border,取最大非平凡border进行比较,如果模式串的结尾超出了主串的长度,则匹配失败
KMP模板
#include<iostream>
#include<cstring>
#define maxn 1000010
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
int nxt[maxn],pos[maxn]; //pos存储成功匹配位置,nxt存储前缀的i最大border
int lenstr,tmp=0,lenss,p=0; //p为匹配位置的个数
char str[maxn],ss[maxn]; //str为文本串,ss为模式串
void clear(){ /*初始化*/
lenss=strlen(ss+1); lenstr=strlen(str+1);
ss[lenss+1]='\0'; str[lenstr+1]='\0';
nxt[0]=nxt[1]=0;
}
void init(){ /*初始化nxt数组*/
for (int i=2;i<=lenss;i++){
nxt[i] = nxt[i-1];
while (nxt[i]&&ss[i]!=ss[nxt[i]+1]) nxt[i] = nxt[nxt[i]];
nxt[i]+=(ss[i]==ss[nxt[i]+1]);
}
}
void kmp(){ /*进行kmp字符串模式匹配*/
for(int i=1;i<=lenstr;i++){
while(tmp>0&&ss[tmp+1]!=str[i]) tmp=nxt[tmp];
if (ss[tmp+1]==str[i]) tmp++;
if (tmp==lenss) {pos[p]=i-lenss+1; p++; tmp=nxt[tmp];}
}
}
void solvekmp(){ /*在str中进行ss模式串匹配并输出匹配位置和模式串border*/
clear(); init(); kmp();
for(int i=0;i<p;i++) cout<<pos[i]<<endl;
for (int i=1;i<=lenss;i++) cout<<nxt[i]<<" ";
}
int main()
{
IOS;
cin>>str+1; //保证从1下标输入
cin>>ss+1;
solvekmp();
return 0;
}
怎么会有题目卡常啊
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+7;
int nxt[maxn];
void init(string s){
int len = s.length();
for(int i=2;i<=len;i++){
nxt[i] = nxt[i-1];
while(nxt[i] && s[i] != s[nxt[i]+1]) nxt[i] = nxt[nxt[i]];
nxt[i] += (s[i] == s[nxt[i]+1]);
}
}
int main(){
ios::sync_with_stdio(false);
string str,ss;cin >> str>>ss;
str = " " + str;
ss = " " + ss;
init(ss);
int lenstr = str.length();
int lenss = ss.length();
int tmp = 0;
for(int i = 1;i < lenstr;i++){
while(tmp && ss[tmp+1] != str[i]) tmp = nxt[tmp];
if(ss[tmp+1] == str[i]) tmp++;
if(tmp == lenss-1){
cout<<i-tmp+1<<"\n";
tmp= nxt[tmp];
}
}
for(int i = 1;i < lenss;i++) cout<<nxt[i]<<" ";
}
标签:tmp,ss,border,next,int,KMP,Border 来源: https://www.cnblogs.com/xushengxiang/p/16606881.html