【暖*墟】 #KMP# 模式匹配的算法流程

本文详细介绍KMP算法原理及其应用案例,包括字符串匹配的基本思想、KMP算法的时间复杂度优势及其实现步骤。并通过多个实际问题展示如何利用KMP算法解决字符串匹配问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.求解类型

字符串匹配。给你两个字符串,寻找其中一个字符串是否包含另一个字符串。

如果包含,返回包含的起始位置。如下面两个字符串:

char *str = "bacbababadababacambabacaddababacasdsd";
char *ptr = "ababaca";

str有两处包含ptr。分别在str的下标10,26处。

“bacbababadababacambabacaddababacasdsd”;
这里写图片描述

 

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)最小循环元长度和最大循环次数(poj1961)

#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/1511/3620/3942

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值