题目传送门:戳我进入ios
KMP算法是用来处理字符串匹配的问题的,也就是给你两个字符串,你须要回答B串是不是A串的子串,B串在A串中出现了几回,B串在A串中出现的位置等问题。算法
KMP算法的意义在于,若是你在洛谷上发了一些话,kkksc03就能够根据KMP算法查找你是否说了一些不和谐的字,而且屏蔽掉你的句子里的不和谐的话(好比cxk鸡你太美就会被屏蔽成cxk****),还会根据你句子中出现不和谐的字眼的次数对你进行处罚spa
举个栗子:A:GCAKIOI B:GC ,那么咱们称B串是A串的子串指针
咱们称等待匹配的A串为主串,用来匹配的B串为模式串。code
通常的朴素作法就是枚举B串的第一个字母在A串中出现的位置并判断是否适合,而这种作法的时间复杂度是O(mn)的,当你处理一篇较长文章的时候显然就会超时。blog
咱们会发如今字符串匹配的过程当中,绝大多数的尝试都会失败,那么有没有一种算法可以利用这些失败的信息呢?ip
KMP算法就是字符串
KMP算法的关键是利用匹配失败后的信息,尽可能减小模式串与主串的匹配次数以达到快速匹配的目的get
设主串(如下称为T)string
设模式串(如下称为W)
用暴力算法匹配字符串过程当中,咱们会把T[0] 跟 W[0] 匹配,若是相同则匹配下一个字符,直到出现不相同的状况,此时咱们会丢弃前面的匹配信息,而后把T[1] 跟 W[0]匹配,循环进行,直到主串结束,或者出现匹配成功的状况。这种丢弃前面的匹配信息的方法,极大地下降了匹配效率。
咱们来看一看KMP是怎么工做的
在KMP算法中,对于每个模式串咱们会事先计算出模式串的内部匹配信息(也就是说这个东西只和模式串有关,能够预处理,这个处理咱们后面会提到),在匹配失败时最大的移动模式串,以减小匹配次数。
好比,在简单的一次匹配失败后,咱们会想将模式串尽可能的右移和主串进行匹配。右移的距离在KMP算法中是如此计算的:在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,而后移动使它们重叠。
咱们用两个指针i和j分别表示A[i-j+1......i]和B[1......j]彻底相等,也就是说i是不断增长的,而且随着i的增长,j也相应的变化,而且j知足以A[j]结尾的长度为j的字符串正好匹配B串的前j个字符,如今须要看A[i+1]和B[j+1]的关系
举个栗子:
T: a b a b a b a a b a b a c b
W:a b a b a c b
当i=j=5时,此时T[6]!=W[6],这代表此时j不能等于5了,这个时候咱们要改变j的值,使得W[1...j]中的前j'个字母与后j'个字母相同,由于这样j变成j'后(也就是将W右移j'个长度)才能继续保持i和j的性质。这个j'显然越大越好。在这里W[1...5]是匹配的,咱们发现当ababa的前三个字母和后三个字母都是aba,因此j'最大也就是3,此时状况是这样
T: a b a b a b a a b a b a c b
W: a b a b a c b
那么此时i=5,j=3,咱们又发现T[6]与W[4]是相等的,而后T[7]与W[5]是相等的(这里是两步)
因此如今是这种状况:i=7,j=5
T: a b a b a b a a b a b a c b
W: a b a b a c b
这个时候又出现了T[8]!=W[6]的状况,因而咱们继续操做。因为刚才已经求出来了当j=5时,j'=3,因此咱们就能够直接用了(经过这里咱们也能够发现j'是多少和主串没有什么关系,只和模式串有关系)
因而又变成了这样
T: a b a b a b a a b a b a c b
W: a b a b a c b
这时,新的j=3依然不能知足A[i+1]=B[j+1],因此咱们还须要取j'
咱们发现当j=3时aba的第一个字母和最后一个字母都是a,因此这时j'=1
新的状况:
T: a b a b a b a a b a b a c b
W: a b a b a c b
仍然不知足,这样的话j须要减少到j'就是0(咱们规定当j=1时,j'=0)
T: a b a b a b a a b a b a c b
W: a b a b a c b
终于,T[8]=B[1],i变为8,j变为1,咱们一位一位日后,发现都是相等的,最后当j=7还知足条件时,咱们就能够下结论:W是T的子串,而且还能够找到子串在主串中的位置(i+1-m+1,由于下标从0开始)
这一部分的代码其实很短,由于用了for循环
inline void kmp() { int j=0; for(int i=0;i<n;i++) { while(j>0&&b[j+1]!=a[i+1]) j=nxt[j]; if(b[j+1]==a[i+1]) j++; if(j==m) { printf("%d\n",i+1-m+1); j=nxt[j]; //当输出第一个位置时 直接break掉 //当输出全部位置时 j=nxt[j]; //当输出区间不重叠的位置时 j=0 } } }
这里就有一个问题:为何时间复杂度是线性的?
咱们从上述的j值入手,由于每执行一次while循环都会使j值减少(但不能到负数),以后j最多+1,所以整个过程当中最多加了n个1.因而j最多只有n个机会减少。这告诉咱们,while循环最多执行了n次,时间复杂度平摊到for循环上后,一次for循环的复杂度是O(1),那么总的时间复杂度就是O(n)的(n是主串长度)。这样的分析对于下文的预处理来讲一样有效,也能够获得预处理的时间复杂度是O(m)(m是模式串长度)
接下来是预处理
预处理并不须要按照定义写成O(m2)甚至O(m3),窝们能够经过nxt[1],nxt[2]....nxt[n-1]来求得nxt[n]的值
举个栗子
W :a b a b a c b
nxt:0 0 1 2 ??
假如咱们有一个串,而且已经知道了nxt[1~4]那么如何求nxt[5]和nxt[6]呢?
咱们发现,因为nxt[4]=2,因此w[1~2]=w[3~4],求nxt[5]的时候,咱们发现w[3]=w[5],也就是说咱们能够在原来的基础上+1,从而获得更长的相同先后缀,此时nxt[5]=nxt[4]+1=3
W :a b a b a c b
nxt:0 0 1 2 3?
那么nxt[6]是否也是nxt[5]+1呢?显然不是,由于w[nxt[5]+1]!=w[6],那么此时咱们能够考虑退一步,看看nxt[6]是否能够由nxe[5]的状况所包含的子串获得,便是否nxt[6]=nxt[nxt[5]]+1?
事实上,这样一直推下去也不行,因而咱们知道nxt[6]=0
那么预处理的代码就是这样的
inline void pre() { nxt[1]=0;//定义nxt[1]=0 int j=0; rep(i,1,m-1) { while(j>0&&b[j+1]!=b[i+1]) j=nxt[j]; //不能继续匹配而且j尚未减到0,就退一步 if(b[j+1]==b[i+1]) j++; //若是能匹配,就j++ nxt[i+1]=j;//给下一个赋值 } }
完整的代码:
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<string> #include<cmath> #include<queue> #include<algorithm> #include<iomanip> using namespace std; #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) typedef long long ll; ll read() { ll ans=0; char last=' ',ch=getchar(); while(ch<'0'||ch>'9') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } char a[1000005],b[1000005]; int nxt[1000005],n,m; inline void pre() { nxt[1]=0; int j=0; rep(i,1,m-1) { while(j>0&&b[j+1]!=b[i+1]) j=nxt[j]; if(b[j+1]==b[i+1]) j++; nxt[i+1]=j; } } inline void kmp() { int j=0; for(int i=0;i<n;i++) { while(j>0&&b[j+1]!=a[i+1]) j=nxt[j]; if(b[j+1]==a[i+1]) j++; if(j==m) { printf("%d\n",i+1-m+1); j=nxt[j]; } } rep(i,1,m) printf("%d ",nxt[i]); } int main() { scanf("%s%s",a+1,b+1); n=strlen(a+1),m=strlen(b+1); pre(); kmp(); return 0; }