【暖*墟】#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)原(短)字符串自我匹配

考察目标字符串ptrababaca

这里我们要计算一个长度为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

【等待填坑中Σ( ° △ °|||)︴】

 

                         ——时间划过风的轨迹,那个少年,还在等你

posted @ 2018-07-19 16:40  花神&缘浅flora  阅读(188)  评论(0编辑  收藏  举报