【暖*墟】#KMP# 模式匹配的算法流程
1.求解类型
字符串匹配。给你两个字符串,寻找其中一个字符串是否包含另一个字符串。
如果包含,返回包含的起始位置。如下面两个字符串:
char *str = "bacbababadababacambabacaddababacasdsd"; char *ptr = "ababaca";
str有两处包含ptr。分别在str的下标10,26处。
2.算法说明
我们从原始字符串str(假设长度为n)的第一个下标、
选取和ptr长度(长度为m)一样的子字符串进行比较。
如果一样,就返回开始处的下标值,不一样,选取str下一个下标。
继续选取长度为m的字符串比较,直到str的末尾(即下标移动到n-m)。
这样的时间复杂度是O(n*m)。
KMP算法:可以实现复杂度为O(m+n)。
充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,
即使不存在重复字段,在比较时,实现最大的移动量)。
(1)原(短)字符串自我匹配
考察目标字符串ptr:ababaca
这里我们要计算一个长度为m的转移函数next。
next数组的含义就是一个固定字符串的 最长前缀 和 最长后缀 相同的长度。
比如:abcjkdabc,那么这个数组的最长前缀和最长后缀相同必然是abc。
cbcbc,最长前缀和最长后缀相同是cbc。
abcbc,最长前缀和最长后缀相同是不存在的。
预处理的数组next:next [ i ] 表示 “str中以i结尾的非前缀字串(上文说的后缀)”
与 “str的前缀” 能够匹配到的最长长度。
// A="ababacb"; 长度为m
// B="abababaababacb"; 长度为n
(注意:以下代码的字符串输入都从1开始)
//next[i] next[1]=0; j=0; for(int i=1;i<m;i++){ //a数组自我匹配,从i+1=2与1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=next[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 next[i+1]=j; //记录这一位向前的最长匹配 }
(2)str,ptr的匹配
数组f:f [ i ] 表示 “ ptr中以 i 结尾的子串 ” 与 “ str的前缀 ” 的最长匹配长度。
在b串中寻找a串出现的位置:
j=0; for(int i=0;i<n;i++){ //扫描b,寻找a的匹配 while(b[i+1]!=a[j+1]&&j>0) j=next[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态) if(b[i+1]==a[j+1]) j++; //匹配加长,j++ if(j==m){ //【一定要把这个判断写在j++的后面!】 printf("%d\n",i+1-m+1); //子串起点在母串b中的位置 j=next[j]; //继续寻找匹配 } //【↑↑巧妙↑↑这里不用返回0,只用返回上一匹配值】 }
求f数组(a与b最大的匹配长度):
//f[i] j=0; for(int i=0;i<n;i++){ //扫描b while(( j==m || b[i+1]!=a[j+1] ) && j>0) j=next[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态) //↑↑↑或a在b中找到完全匹配 if(b[i+1]==a[j+1]) j++; //匹配加长,j++ f[i+1]=j; //此位置和先前组成的最长匹配 // (if(f[i+1]==m),此时a在b中找到完全匹配) }
3.例题与相关分析
(1)剪花布条 hdu2087
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; //【剪花布条】能从花布条中剪出多少小花条? [注意:不能重叠!] char a[1009],b[1009]; int nextt[1009],n,m; void pre(){ //预处理:求next[i] nextt[1]=0; int j=0; for(int i=1;i<m;i++){ //b数组自我匹配,从1与前一位0比较开始 while(j>0&&b[i+1]!=b[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(b[i+1]==b[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } } int kmps(){ //在a串中寻找b串的出现次数 int ans=0,j=0; for(int i=0;i<n;i++){ //扫描a while(b[j+1]!=a[i+1]&&j>0) j=nextt[j]; //↑不能继续匹配且j还没减到0(之前的匹配有剩余状态) if(j==m){ ans++; j=0; } //j=0,保证不重叠 if(b[j+1]==a[i+1]) j++; //匹配加长,j++ } return ans; } int main(){ while(cin>>a+1){ //从1开始读入字符串a if(a[1]=='#') break; //输入结束 scanf("%s",b+1); m=strlen(b+1),n=strlen(a+1); pre(); printf("%d\n",kmps()); } return 0; }
(2)字符串周期(poj 2406)
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【power strings】poj2406 给出一个不超过1e6的字符串,求这个字符串最多有多少个周期。 */ char a[1000005]; int nextt[1000005],n; void pre(){ //nextt[i] nextt[1]=0; int j=0; for(int i=1;i<n;i++){ //a数组自我匹配,从i+1=2与1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } } int main(){ while(1){ scanf("%s",a+1); if(a[1]=='.') break; n=strlen(a+1); pre(); if(n%(n-nextt[n])==0) //nextt的定义 //nextt[i]表示 “str中以i结尾的非前缀字串(某段后缀)” //与 “str的前缀” 能够匹配到的最长长度。 printf("%d\n",n/(n-nextt[n])); else printf("1\n"); } return 0; }
(3)最小循环元长度和最大循环次数(poj 1961)
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【period】poj 1961 给你一个字符串,求这个字符串到第i个字符为止的最小循环元长度和最大循环次数。 */ char a[1000005]; int nextt[1000005],n,T; void pre(){ //nextt[i] nextt[1]=0; int j=0; for(int i=1;i<n;i++){ //a数组自我匹配,从i+1=2与1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } } int main(){ while(cin>>n&&n){ scanf("%s",a+1); pre(); printf("Test case #%d\n",++T); //T从1开始 for(int i=2;i<=n;i++){ if(i%(i-nextt[i])==0&&i/(i-nextt[i])>1) printf("%d %d\n",i,i/(i-nextt[i])); } printf("\n"); } return 0; }
(4)bzoj 1355 / 洛谷 p4391
#include<iostream> #include<cstdio> #include<algorithm> #include<string> #include<cstring> #include<cmath> #include<map> #include<queue> using namespace std; /*【p4391】无线传输 给你一个字符串,它是由某个字符串不断自我连接形成的。 但是这个字符串是不确定的,现在只想知道它的最短长度是多少. */ //设最短的长度为x,那么前x个next数组的值为0。 //next[x+1]=1,next[x+2]=2...因此,x=n-next[n]。 char ss[1000006]; int j,n,nextt[1000006]; int main(){ cin>>n; scanf("%s",ss+1); for(int i=2;i<=n;i++){ j=nextt[i-1]; while(j&&ss[j+1]!=ss[i]) j=nextt[j]; if(ss[j+1]==ss[i]) j++; nextt[i]=j; } cout<<n-nextt[n]<<endl; return 0; }
(5)bzoj 1511 / 洛谷 p3435
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<string> #include<cmath> using namespace std; typedef long long ll; /*【洛谷p3435】Periods of Words 一个串是有限个小写字符的序列,特别的,一个空序列也可以是一个串。 定义Q 是A的周期, 当且仅当Q是A的一个非空前缀并且A是QQ的前缀。 比如串abab和ababab都是串abababa的周期。注意周期也可以是空串。 给出一个串,求出它所有前缀的最大周期长度之和。*/ //数组next[i]表示前缀i中【前缀】和【后缀】相等的最长长度。 //那么,如果i的某个公共前后缀长度为j,就说明有周期长度为i-j。 //我们可以先在整串a中求出next数组。对于每个前缀i, //令j=i,然后在j>0的情况下,不断令j=next[j],寻找最小的j,此时ans+=i−j。 //【优化】对于每个i、求出j以后,令j=nextt[i], // 这样再次访问nextt[i]时,可以直接找到j(记忆化)。 void reads(int &x){ //读入优化(正负整数) int fx=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; //正负号 } char a[1000009]; int nextt[1000009],n; void pre(){ //预处理:求next[i] nextt[1]=0; int j=0; for(int i=1;i<n;i++){ //a数组自我匹配,从2与前一位1比较开始 while(j>0&&a[i+1]!=a[j+1]) j=nextt[j]; //↑自身无法继续匹配且j还没减到0,考虑返回匹配的剩余状态 if(a[i+1]==a[j+1]) j++; //这一位匹配成功 nextt[i+1]=j; //记录这一位向前的最长匹配 } } int main(){ ll ans=0; scanf("%d",&n); scanf("%s",a+1); pre(); for(int i=1;i<=n;i++){ int j=i; while(nextt[j]>0) j=nextt[j]; //寻找最小的公共前缀后缀匹配串 ans+=i-j; if(nextt[i]!=0) nextt[i]=j; } cout<<ans<<endl; }
(6)bzoj 3620/3942
【等待填坑中Σ( ° △ °|||)︴】
——时间划过风的轨迹,那个少年,还在等你