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 LOCALfreopen("input.txt","r",stdin);freopen("output.txt","w",stdout);#endifstring 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]<<" ";
}
KMP字符串模式匹配相关推荐
- KMP字符串模式匹配详解
KMP字符串模式匹配详解 KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法.简单匹配算法的时间复杂度为O(m*n);KMP匹配算法.可以证明它的时间复杂度为O(m+n).. 一 ...
- KMP字符串模式匹配详解(zz)
刚看到位兄弟也贴了份KMP算法说明,但本人觉得说的不是很详细,当初我在看这个算法的时候也看的头晕昏昏的,我贴的这份也是网上找的. 且听详细分解: KMP字符串模式匹配详解 来自CSDN A_B ...
- KMP算法字符串模式匹配
KMP字符串模式匹配详解 来自CSDN A_B_C_ABC 网友 KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法.简单匹配算法的时间复杂度为O(m*n);KMP匹配算 ...
- 字符串模式匹配——最长公共子序列与子串 KMP 算法
最长公共子序列 最长公共子序列的问题很简单,就是在两个字符串中找到最长的子序列,这里明确两个含义: 子串:表示连续的一串字符 . 子序列:表示不连续的一串字符. 所以这里要查找的是不连续的最长子序列, ...
- 字符串模式匹配--KMP之美
字符串模式匹配: 给定字符串,要求在该字符串(主串)中找到所有匹配一个模式串的子串(一般是返回子串在字符串中的开头位置).这里把问题简化一下--在该字符串中找到第一个匹配对应模式串的子串即可.要找出剩 ...
- 数据结构之字符串模式匹配
程序源代码:点击打开链接 1.引入 字符串模式匹配.首先我们引入目标串,模式串的概念,而字符串模式匹配就是查找模式串在目标串中的位置. 2.brute-Force算法 brute-Force算法,我的 ...
- kmp字符串查询算法
kmp字符串查询算法 1 普通的字符串查询 普通的字符串查询是遍历被查找的字符串,然后和key字符串进行匹配,如果不一致,则,被查找的字符串+1,继续向下遍历. 代码如下: private stati ...
- 数据结构---BF字符串模式匹配
数据结构-BF字符串模式匹配 原理:参考趣学数据结构 代码: #include<stdio.h> #include<stdlib.h> int BF(char * S, cha ...
- 【算法视频】字符串模式匹配--布鲁特.福斯算法
2.4.字符串模式匹配 资讯网址:www.qghkt.com 腾讯课堂:https://qghkt.ke.qq.com/20个常用算法 模式串(或子串)在主串中的定位操作通常称为串的模式匹配,它是各种 ...
最新文章
- spring的jar各包作用
- 视频聊天创企Tribe获300万美元种子轮融资
- android 绑定端口号,android 获取IP端口号等地址
- 热烈欢迎乔丹入驻博客园
- 跟我学JAVA / 第三课:Java流程控制与数组
- javaweb成长之路:struts2的探索(一)
- OpenCV学习--saturate_cast防止数据溢出
- javascript json_爬虫里总要用到的 JSON 是什么?
- CUDA系列学习(四)Parallel Task类型 与 Memory Allocation
- hbase的备份恢复1,Expor过程,Import过程,统计hbase表行数;hbase备份恢复方式2:使用hdfs备份hbase数据,基于hbase数据进行恢复
- Qt在VS2012中引用QtWidgets时报GLES2/gl2.h无法打开错误的解决办法
- 解决SVN安装语言包后无法选择中文的问题
- Java 经纬度计算两个点的之间的距离工具类
- 推荐一款最近发现非常实用的数据库建模工具
- 最常用的scrum工具、敏捷开发工具、看板工具
- .Net 文件名后缀的详细解释
- Coursera吴恩达《构建机器学习项目》课程笔记(2)-- 机器学习策略(下)
- CS5218: DP转HDMI 4K30HZ转换方案
- cannot reach adb server, attempting to reconnect.
- VS2008编译时error C2248处理方法