KMP 算法求字符串匹配
KMP 算法求字符串匹配
单模式串匹配
基本术语
字母表上的字符用小写字符 a,b,c ...来表示
字符串用大写的S,T,W...来表示
字符串的长度,用 | S | 来表示
字符串的连接,记为ST
空串,记位 ε
一些定义
前缀:设S=XY(X,Y皆可为空),称X为S的前缀
后缀:设S=XY(X,Y皆可为空),称Y为S的后缀
子串:设S=XWY(X,W,Y皆可为空),称X,W,Y为S的子串.因此,空串可以是任意一个字符串的字串. 如何分片? 将S中位置i到位置j的这一子串记为S[i,j].
匹配
若W是是S的子串,称W匹配了S,称S为主串或文本(串),W为子串或模式串
字符串匹配分为可相交匹配与不可相交匹配两种
KMP算法就是一个求字符串匹配的算法
KMP算法
为什么会出现KMP算法
朴素算法的思路是依次对齐位置,挨个检查每个字符,失配就会继续向后对其. 很显然在最坏的情况下,会运行(|s|)方次,即其时间复杂度为O(x^2).如遇10000以上数据便基本超时
分析
朴素算法之所以慢,是因为其算法本身没有很好地利用已扫描的内容,导致每一次都只能机械的扫描,每一次适配也只能将模式串机械的向后移动一位而不是动态的。
而KMP算法就能解决这个问题,其扫描与移动是动态进行的,其每一次操作除了移动模式串,就是移动模式串上的指针且保证模式串上指针之前的部分都保证匹配上。保证每一次操作都至少向后移动一个字符,总共移动。时间复杂度为O(n).(初始化的过程时间复杂度可以忽略)
算法本身
这个算法的关键在于模式串每一次操作后移的长度(仅当这个长度与未扫描到的字符无关)
对于一个模式串 S=ababcdefg,指针当前指向c。
抽离c之前的子串S[0,3]=abab 发现该子串的前缀与后缀都是 ab。所以说将此字符串向后移动2位,指针移动至2后该串的前两位仍能匹配。
进一步发现,凡字符串满足S=X Y X向后移动|X+Y|位,指针指向|X|后仍然能将前|X|位匹配上。
以这种方式即可大幅的降低时间复杂度(好似很简单
border,fail及其性质
设X≠S满足X是S的前缀,且是S的后缀,则称X是S的一个border。S的所有border之集记作border(S)。border(S)中长度最大的X记作Border(S)。显然ε是任何非空串的border。很显然,一个串最多有n个border(包括ε)
设|S|=n,则fail数组是一个有n个元素的数组,其中
fail[i]=|Border(S[0,i])|,规定fail[0]=0。显然fail[i]≤i。
举个栗子:对于一个串 S=a b a c a b a ,fail={0,0,1,0,1,2,3}
当然一个字符串有可能出现多个border
如:S=X1 Y1 X1=X2 Y2 X2=X3 Y3 X3=X4 Y4 X4=...=ε Yn ε(X的大小递减即|X1|=fail[|S|-1],Y的值递增,|Yn|=|S|)
移动时如何选择呢?
每次移动只选择第一种即X1=Border(S).
为什么?
通过画图可得Border(X1 )=X2,Border(X2)=X3,Border(X3)=X4... 所以选择最大的一种进行移动可以遍历到所有的情况
如何求fail?
画图结合代码便知
具体实现
初始化:匹配长度j=0,指针i=0;
重复以下步骤,直到指针i达到主串末尾:
(1)若当前指针位置匹配,则i++;此时若j达到模式串末尾,则匹配成功!
(2)否则,重复j=fail[j-1],直到当前指针位置匹配;或者j=0。
代码实现
#include<iostream>
using namespace std;
const int maxn=1000007;
char st[maxn]; //定义模式串
char s[maxn];
int fail[maxn];
int siz=1;
void init()//初始化fail数组
{
fail[0]=0; //根据定义,fail[0]=0
for(int i=1,j=0;st[i];i++){ //i是迭代变量,j存储fail[i-1]
while(j&&st[i]!=st[j]){ //搜索合适的Border
j=fail[j-1];
}
if(st[i]==st[j]){ //找到Border(i)
fail[i]=++j;
}
siz++;
}
}
int search(char *str) //KMP算法的主程序(搜索匹配)部分,str为主串
{
int ans=0;
for(int i=0,j=0;str[i];i++){ //i为主串指针,j为模式串指针
while(j&&str[i]!=st[j]) //搜索合适的Border
j=fail[j-1];
if(str[i]==st[j]){
if(!st[++j]){
ans++;
}
}
}
return ans;
}
int main()
{
cin>>s;
cin>>st;
init();
int ans=search(s); //search函数的返回值为出现的次数
cout<<ans<<endl;
}
看看有什么练习
很简单的一道板子题,只需跑一边模板就可以A了
代码如下
#include<iostream>
using namespace std;
const int maxn=1000007;
int on_off;
char st[maxn];
char s[maxn];
int fail[maxn];
int siz=1;
void init()
{
fail[0]=0;
for(int i=1,j=0;st[i];i++){
while(j&&st[i]!=st[j]){
j=fail[j-1];
}
if(st[i]==st[j]){
fail[i]=++j;
}
siz++;
}
}
int search(char *str)
{
int ans=0;
for(int i=0,j=0;str[i];i++){
while(j&&str[i]!=st[j])
j=fail[j-1];
if(str[i]==st[j]){
if(!st[++j]){
ans++;
cout<<i-siz+2<<endl;
}
}
}
return ans;
}
int main()
{
cin>>s;
cin>>st;
init();
int ans=search(s);
for(int i=0;i<siz;i++){
cout<<fail[i]<<" ";
}
}
luoguP3435 [POI2006]OKR-Periods of Words
看完题后,经过分析可发现: 前缀 S 如果存在一个border = T,它会有一个长为|S|-|T|的周期。
为了使该前缀的周期最大,就要使|T|最小.但由于T是S的一个proper前缀,所以|S|>|T|. 跳fail数组记忆化找就是了。
border是可能有很多的. f[i]存的是主串中从开头到结尾下标为i的子串的非零的最小公共前后缀的长度。这个是从0开始求的。当i为0时求得的肯定是最优解。因为前面是根据最优解推来的,后面通过f[fail[i]]求出的f[i]也应该是最优。
代码如下:
#include<iostream>
using namespace std;
const int maxn=1000007;
long long ans;
char st[maxn];
long long k;
long long fail[maxn];
long long f[maxn];
void init()
{
fail[0]=0;
for(int i=1,j=0;st[i];i++){
while(j&&st[i]!=st[j]){
j=fail[j-1];
}
if(st[i]==st[j]){
fail[i]=++j;
}
}
}
int main()
{
cin>>k;
cin>>st;
init();
for(int i=0;i<k;i++){
if(fail[i]){
f[i]=f[fail[i]-1];
}
else f[i]=i+1;
ans+=i+1-f[i];
}
cout<<ans;
}