KMP&exKMP学习笔记

KMP

KMP 算法,是一种可以在 O ( n + m ) O(n+m) O(n+m) 的时间复杂度内实现字符串匹配的算法。

指针 i i i j j j 定义:a[i-j+1,i]b[1,j] 匹配。匹配随着 i i i 的增加, j j j 也在增加。当 j = m j=m j=m 时,完全匹配。

我们用一个数组 nxt[i],表示对于一个长度为 i i i 的字符串,从前数和从后数都相同的子串的最大长度。

从朴素算法,即一个一个移位尝试匹配可以推知,为了降低时间复杂度,我们需要利用已经匹配过的部分。这时候就会用到 nxt[i] 数组。


练手板子题

代码如下:

#include <bits/stdc++.h>
using namespace std;

const int maxn=1e6+5;
char a[maxn],b[maxn];
int nxt[maxn],n,m;

int main()
{
	scanf("%s%s",a+1,b+1);
	n=strlen(a+1),m=strlen(b+1);
 //注意这里长度的<=,因为我的下标是从1开始的
	for(int i=1,j=0;i<=m;i++)
	{
		while(j&&b[i+1]!=b[j+1]) j=nxt[j];
		if(b[i+1]==b[j+1]) j++;
		nxt[i+1]=j;
	}
	for(int i=0,j=0;i<=n;i++)
	{
		while(j&&a[i+1]!=b[j+1]) j=nxt[j];//由于j<=nxt[j],不妨设j=nxt[j]
     //这一步是一个递归操作,如果当前位不能匹配,就要去找nxt[j]
     //j不等于0,防止死循环
		if(a[i+1]==b[j+1]) j++;//如果当前位能匹配
		if(j==m) cout<<(i+2-m)<<endl;
     //关于这里为什么是i+2-m,由于当前希望匹配的是i+1位,如果下一位匹配成功且j++之后等于m,我们把i移一位再减去模式串长度再加1
	}
	for(int i=1;i<=m;i++) cout<<nxt[i]<<" ";
	return 0;
}

失配数组

nxt数组是KMP算法的关键与精髓,让我们再细细地品一下nxt数组的预处理。nxt数组其实是为了简化朴素算法的移动过程,如下图所示:

匹配完前 3 3 3 个字符串 ABC \texttt{ABC} ABC 后,我们不需要像朴素算法一样只把 i i i j j j 指针各向右移动 1 1 1 位,我们可以不移动 i i i 指针,移动 j j j 指针至下一个可能匹配的位置,这样可以优化时间复杂度。

而对于这样的移动,假设指针 j j j 要移动到的位置是 k k k,模式串(即下面那个短一些的子串)的前 k k k 位和指针 j j j 之前的 k k k 位相等,而这就是 nxt 数组的含义。nxt[j] 表示的就是 a[i+1]!=b[j+1] 时, j j j 指针移动到的位置。

注意这里根据 nxt 数组进行移动的是短串 b b b

for(int i=0,j=0;i<=n;i++)
	{
		while(j&&a[i+1]!=b[j+1]) j=nxt[j];//由于j<=nxt[j],不妨设j=nxt[j]
        //这一步是一个递归操作,如果当前位不能匹配,就要去找nxt[j]
        //j不等于0,防止死循环
		if(a[i+1]==b[j+1]) j++;//如果当前位能匹配
		if(j==m) cout<<(i+2-m)<<endl;
	}

对于找一个字符串的 nxt 数组,我们可以考虑把它自己和自己进行匹配,为了不让两个相同的字符串匹配,我们可以让 i i i 1 1 1 开始,实现两个串错开一位。其他操作和不同字符串的求 nxt 数组流程相同。

for(int i=1,j=0;i<=m;i++)//i从1开始,两个相同串错开
{
    while(j&&b[i+1]!=b[j+1]) j=nxt[j];//不能匹配,递归操作
    if(b[i+1]==b[j+1]) j++;
    nxt[i+1]=j;
}

扩展

  • nxt 数组求最小循环节公式,若 n%(n-nxt[n])==0,最小循环节为 n-nxt[n],否则无最小循环节
  • 失配树

exKMP

在学扩展 KMP 之前,我们首先要知道 Z 函数是个什么东西。对于一个长度为 n n n 的字符串 s s s,我们定义 Z 函数 z ⁡ ( i ) \operatorname{z}(i) z(i) s s s s s s i i i 开始的后缀的最长公共前缀(LCP)的长度。特别地, z ⁡ ( 0 ) = 0 \operatorname{z}(0)=0 z(0)=0

理解了 nxt 数组,我们就理解了 KMP;而理解了 Z 函数,我们就理解了扩展 KMP。

对于 Z 函数的预处理,我们可以在 O ( n ) O(n) O(n) 的时间复杂度内完成。

先考虑一种蠢蠢的 O ( n 2 ) O(n^2) O(n2) 的朴素做法预处理 Z 函数的值 显然一定炸

int z[520];
char s[520];
    
z[0]=0;
for(int i=1;i<=len;i++)
    while(i+z[i]<=n&&s[z[i]]==s[i+z[i]]) ++z[i];

于是乎我们考虑一些神奇的优化,把时间复杂度搞到线性的 O ( n ) O(n) O(n)

我们考虑能否在计算 z ⁡ ( i ) \operatorname{z}(i) z(i) 时利用 z ⁡ ( 0 ) \operatorname{z}(0) z(0) z ⁡ ( i − 1 ) \operatorname{z}(i-1) z(i1) 的值。

对于 i i i,定义区间 [ i , i + z ⁡ ( i ) − 1 ] [i,i+\operatorname{z}(i)-1] [i,i+z(i)1] i i i 的匹配段(Z-box)。

我们维护一个区间 [ l , r ] [l,r] [l,r],让它等于右端点最靠后的 Z-box。为什么要这么做捏?其实是为了保证时间复杂度足够优。

让指针从 0 0 0 n − 1 n-1 n1 遍历,维护区间 [ l , r ] [l,r] [l,r]。注意 l l l r r r 的初始值都为 0 0 0,并且对于遍历的每一个 i i i 都要满足 l ≤ i l\leq i li,对于 r r r,我们可以让 r r r 尽量大。怎么样是不是有一种 KMP 的感觉了,找一个前后缀相同的区间,具体是这样的:

(由于这只蒟蒻不会自己画图所以他不得不从别人的博客里借图)

当前选到的 i i i 11 11 11 的位置上,于是我们进行一个类似于 KMP 的移位操作:

显然可以发现,我们的紫色框框里的两位是已经匹配上的。考虑以下情况:

i ≤ r i\leq r ir 时, s [ i , r ] s[i,r] s[i,r] s [ i − l , r − l ] s[i-l,r-l] s[il,rl] 是相等的,于是乎我们求此时的 z ⁡ ( i ) \operatorname{z}(i) z(i) z ⁡ ( i ) ≥ min ⁡ ( z ⁡ ( i − l ) , r − i + 1 ) \operatorname{z}(i)\geq \min(\operatorname{z}(i-l),r-i+1) z(i)min(z(il),ri+1)。想一想,为什么是这样?首先,如果说 z ⁡ ( i − l ) < r − i + 1 \operatorname{z}(i-l)<r-i+1 z(il)<ri+1,那么匹配区间即黄色部分不会超出,于是乎我们就可以直接利用 z ⁡ ( i ) = z ⁡ ( i − l ) \operatorname{z}(i)=\operatorname{z}(i-l) z(i)=z(il);如果超出区间了话,先更新到 r − i + 1 r-i+1 ri+1,然后再暴力枚举下一个字符;

i > r i>r i>r 时,我们考虑用朴素算法,从 s [ i ] s[i] s[i] 开始比较,暴力求出 z ⁡ ( i ) \operatorname{z}(i) z(i)

求出 z ⁡ ( i ) \operatorname{z}(i) z(i) 之后,尝试更新区间右端点:如果 i + z ⁡ ( i ) − 1 > r i+\operatorname{z}(i)-1>r i+z(i)1>r,更新 [ l , r ] [l,r] [l,r],令 l = i l=i l=i r = i + z ⁡ ( i ) − 1 r=i+\operatorname{z}(i)-1 r=i+z(i)1

如果还不懂的,可以看看这两张图,是讲题时的板书:

现在看,其实 Z 函数更新右端点等操作和 Manacher 算法有着异曲同工之处,它们的时间复杂度也相同,都是线性的。

求 Z 函数的代码实现如下:

scanf("%s%s",a,b);
int la=strlen(a),lb=strlen(b);
zb[0]=lb;
for(int i=1,l=0,r=0;i<lb;++i)
{
    if(i<=r&&zb[i-l]<r-i+1) zb[i]=zb[i-l];//因为不确定下一位是否相等,所以直接继承z[i-l]
    else
    {
        zb[i]=max(0,r-i+1);//0说明i>r,r-i+1说明超出匹配区间
        while(i+zb[i]<lb&&b[zb[i]]==b[i+zb[i]]) ++zb[i];
    }
    if(i+zb[i]-1>r) l=i,r=i+zb[i]-1;
}

接下来考虑如何求第二个问题: b b b a a a 的每一个后缀的 LCP 长度数组,其实就是 a a a 关于模板 b b b 的 Z 函数。只需要改改代码就行啦!由于是 a a a b b b 的 Z 函数,取的要是 b b b 的 Z 函数。

for(int i=0;i<la;++i)//求a关于模板串b的z[0]
{
    if(b[i]==a[i]) ++za[0];
    else break;
}
for(int i=1,l=0,r=0;i<la;++i)
{
    if(i<=r&&zb[i-l]<r-i+1) za[i]=zb[i-l];
    else
    {
        za[i]=max(0,r-i+1);
        while(i+za[i]<la&&b[za[i]]==a[i+za[i]]) ++za[i];
    }
    if(i+za[i]-1>r) l=i,r=i+za[i]-1;
}

全部代码如下(因为是板子就全放出来了):

#include <bits/stdc++.h>
using namespace std;

const int maxn=2*1e7+5;
char a[maxn],b[maxn];
long long za[maxn],zb[maxn];

int main()
{
	scanf("%s%s",a,b);
	int la=strlen(a),lb=strlen(b);
	zb[0]=lb;
	for(int i=1,l=0,r=0;i<lb;++i)
	{
		if(i<=r&&zb[i-l]<r-i+1) zb[i]=zb[i-l];//因为不确定下一位是否相等,所以直接继承z[i-l]
		else
		{
			zb[i]=max(0,r-i+1);//0说明i>r,r-i+1说明超出匹配区间
			while(i+zb[i]<lb&&b[zb[i]]==b[i+zb[i]]) ++zb[i];
		}
		if(i+zb[i]-1>r) l=i,r=i+zb[i]-1;
	}
	for(int i=0;i<la;++i)//求a关于模板串b的za[0]
	{
		if(b[i]==a[i]) ++za[0];
		else break;
	}
	for(int i=1,l=0,r=0;i<la;++i)
	{
		if(i<=r&&zb[i-l]<r-i+1) za[i]=zb[i-l];
		else
		{
			za[i]=max(0,r-i+1);
			while(i+za[i]<la&&b[za[i]]==a[i+za[i]]) ++za[i];
		}
		if(i+za[i]-1>r) l=i,r=i+za[i]-1;
	}
	long long ans=0,bp=0;
	for(int i=0;i<la;i++) ans^=(i+1)*(za[i]+1);
	for(int i=0;i<lb;i++) bp^=(i+1)*(zb[i]+1);
	cout<<bp<<endl<<ans;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值