ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

KMP

2022-08-19 23:33:58  阅读:165  来源: 互联网

标签:tmp ss border next int KMP Border


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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有