KMP--学习笔记

Q:给定两个字符串A、B,求A在B中出现了多少次
这是KMP的经典问题,我们将从这里开始引入KMP算法


KMP算法の思想

KMP算法分为两步

1.对A进行自我匹配,求出A的nxt数组
其中 n x t [ i ] nxt[i] nxt[i]表示 A中以 i i i结尾的非前缀子串A的前缀最大匹配长度
即若 n x t [ i ] = j nxt[i]=j nxt[i]=j,则 A [ i − j + 1 A[i-j+1 A[ij+1~ i ] = A [ 1 i]=A[1 i]=A[1 ~ j ] j] j]

2.将A与B匹配,求出数组 f f f
其中 f [ i ] f[i] f[i]表示 B中以 i i i结尾的子串A的前缀最大匹配长度

煮个栗子
在这里插入图片描述

那么该如何快速求出nxt数组呢
假设我们已经知道 n x t [ 1 ] nxt[1] nxt[1]~ n x t [ 6 ] nxt[6] nxt[6],现在求 n x t [ 7 ] nxt[7] nxt[7]

n x t [ 7 ] nxt[7] nxt[7]得过程实际上就是 求一个最大的 j j j,满足 A [ 1 A[1 A[1~ j ] = A [ i − j j] = A[i-j j]=A[ij~ 6 ] 6] 6] A [ j + 1 ] = A [ 7 ] A[j+1]=A[7] A[j+1]=A[7]
然后令 n x t [ 7 ] = j + 1 nxt[7]=j+1 nxt[7]=j+1

对于这个找 j j j得过程,我们可以按照从大到小的顺序
先找到所有满足第一个条件的 j j j再判断第二个条件是否成立

对于 n x t [ 7 ] nxt[7] nxt[7],满足第一个条件的最大的 j j j显然是 j = n x t [ 6 ] = 4 j=nxt[6]=4 j=nxt[6]=4
然后我们判断第二个条件 A [ j + 1 = 5 ] = A [ i = 7 ] A[j+1=5]=A[i=7] A[j+1=5]=A[i=7]成立,所以 n x t [ 7 ] = 5 nxt[7]=5 nxt[7]=5

在考虑 n x t [ 8 ] nxt[8] nxt[8],同样先取最大的满足条件一的 j = n x t [ 7 ] = 5 j=nxt[7]=5 j=nxt[7]=5
但这时候发现 A [ j + 1 = 6 ] ! = A [ i = 8 ] A[j+1=6]!=A[i=8] A[j+1=6]!=A[i=8],条件二不成立
于是我们再找第二大的满足条件一 j j j,而这个 j j j正好就是 n x t [ n x t [ 7 ] ] = n x t [ 5 ] = 3 nxt[nxt[7]]=nxt[5]=3 nxt[nxt[7]]=nxt[5]=3
但是 A [ j + 1 = 4 ] ! = A [ i = 8 ] A[j+1=4]!=A[i=8] A[j+1=4]!=A[i=8],条件二依然不成立
再找第三大的满足条件一 j j j,这个 j j j等于 n x t [ n x t [ n x t [ 7 ] ] ] = n x t [ 3 ] = 1 nxt[nxt[nxt[7]]]=nxt[3]=1 nxt[nxt[nxt[7]]]=nxt[3]=1
很可惜条件二还是不成立
再找第四大的满足条件一 j = n x t [ 1 ] = 0 j=nxt[1]=0 j=nxt[1]=0,到这里前面已经没有字符了
所以直接判断是否有 A [ 1 ] = A [ i = 8 ] A[1]=A[i=8] A[1]=A[i=8],这里满足了条件,所以 n x t [ 8 ] = 1 nxt[8]=1 nxt[8]=1


通过上面的 栗子 我们已经对KMP的思路有了基本了解
在这里再一次用稍形式化的语言描述一次

假设当前已求出 n x t [ 1 ] nxt[1] nxt[1]~ n x t [ i − 1 ] nxt[i-1] nxt[i1]
现在需要求一个最大的 j j j,满足 A [ 1 A[1 A[1~ j ] = A [ i − j j] = A[i-j j]=A[ij~ i − 1 ] i-1] i1] A [ j + 1 ] = A [ i ] A[j+1]=A[i] A[j+1]=A[i]
n x t [ i ] = j + 1 nxt[i]=j+1 nxt[i]=j+1

首先尝试最大的满足条件一的 j = n x t [ i − 1 ] j=nxt[i-1] j=nxt[i1],判断条件二是否成立
若成立则 n x t [ i ] = j + 1 nxt[i]=j+1 nxt[i]=j+1,否则 j = n x t [ j ] j=nxt[j] j=nxt[j],重复上述步骤
j = 0 j=0 j=0时直接判断是否有 A [ 1 ] = A [ i ] A[1]=A[i] A[1]=A[i],若是则 n x t [ i ] = 1 nxt[i]=1 nxt[i]=1,否则等于0

算法的总时间复杂度为 O ( n ) O(n) O(n)


KMPの代码实现

由于一般情况下读入字符串时第一个字符会储存在第0号位
为了方便运算我们将nxt[]表示的值都-1,但记得实际上表示的长度应该再+1

void qnxt(char* ss,int len)
{
    int j=-1; nxt[0]=-1;
    for(int i=1;i<len;++i)
    {
        while(j!=-1&&ss[i]!=ss[j+1]) j=nxt[j];
        if(ss[i]==ss[j+1]) j++;
        nxt[i]=j;
    }
}

求完 n x t [ ] nxt[] nxt[]数组后在考虑 f [ ] f[] f[]数组
由于其定义的基本相同,所以求解方法已基本一致
注意为了运算方便保存在 f [ ] f[] f[]中的值也是减了1的

void KMP(char* a,char* b)
{
    int n=strlen(a),m=strlen(b);
    int j=-1; qnxt(a,n);
    for(int i=0;i<m;++i)
    {
        while(j!=-1&&b[i]!=a[j+1]) j=nxt[j];
        if(b[i]==a[j+1]) j++;
        f[i]=j;
    }	
}

再次提醒,真正的 n x t [ ] nxt[] nxt[] f [ ] f[] f[]保存的值实际应为

for(int i=0;i<strlen(A);++i)
printf("%d ",nxt[i]+1);

for(int i=0;i<strlen(B);++i)
printf("%d ",f[i]+1);

KMPの应用
POJ - 3461 Oulipo

现在再次回到开头的问题 (是不是都快忘掉了)
Q:给定两个字符串A、B,求A在B中出现了多少次

A:求出B的 f [ ] f[] f[]数组后
若有 f [ i ] = n f[i]=n f[i]=n,那么A在B中的一个出现位置就是 B [ i − n + 1 B[i-n+1 B[in+1~ i ] i] i]

#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long lt;

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return f*x;
}

const int maxn=2000010;
int Q;
char txt[maxn],pat[maxn];
int nxt[maxn];

void qnxt(char* ss,int len)
{
	int j=-1; nxt[0]=-1;
	for(int i=1;i<len;++i)
	{
		while(j!=-1&&ss[i]!=ss[j+1]) j=nxt[j];
		if(ss[i]==ss[j+1]) ++j;
		nxt[i]=j;
	}
}

int KMP(char* a,char* b)
{
	int res=0;
	int n=strlen(a),m=strlen(b);
	int j=-1; qnxt(a,n);
	for(int i=0;i<m;++i)
	{
		while(j!=-1&&b[i]!=a[j+1]) j=nxt[j];
		if(b[i]==a[j+1]) ++j;
		if(j==n-1) res++;//f[i]=j
	}
	return res;
}

int main()
{
    Q=read();
    while(Q--)
    {
    	scanf("%s%s",&pat,&txt);
    	printf("%d\n",KMP(pat,txt));
	}
}

POJ - 2752 Seek the Name, Seek the Fame

Q:给定字符串S,求S中既是前缀也是后缀的子串的所有可能长度

A:求出S的nxt数组
j = l e n j=len j=len,不断迭代 j = n x t [ j ] j=nxt[j] j=nxt[j]直到 j = 0 j=0 j=0,遍历到的数即为所求
注意这样得到的顺序时降序的,还需要存入栈中再输出

#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long lt;

int read()
{
    int f=1,x=0;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return f*x;
}

const int maxn=1000010;
char ss[maxn];
int nxt[maxn],vis[maxn];
int st[maxn],top;

void qnxt(char* a,int len)
{
    int j=-1; nxt[0]=-1;
    for(int i=1;i<len;++i)
    {
        while(j!=-1&&a[i]!=a[j+1]) j=nxt[j];
        if(a[i]==a[j+1]) j++;
        nxt[i]=j;
    }
}

int main()
{
    
	while(scanf("%s",&ss)!=EOF)
    {
    	int len=strlen(ss); top=0;
		qnxt(ss,len);

        int j=len-1;
		while(j!=-1){
		    st[++top]=j+1;
		    j=nxt[j];
		}
		while(top) printf("%d ",st[top--]);
		printf("\n");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值