其他分享
首页 > 其他分享> > KMP

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。

屏幕截图 2022 08 15 220650

蓝色部分代表字符串的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 匹配
假设在暴力匹配的过程中发生了如下情况:

屏幕截图 2022 08 15 230332

T1为要搜索的串,S代表被搜索的串

绿色部分表示匹配成功,空格表示匹配失败

由于红蓝方块位置的字符不匹配,因此需要合理向右移动T 字符串,在成功匹配了绿色方块位置的字符之后,才可以继续向后匹配:

就是移动绿色的条带,使得方块的位置能够匹配上,后面的位置才有可能继续匹配

屏幕截图 2022 08 15 230455
此时可以清晰的看到,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