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(i−1) 的值。
对于 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 n−1 遍历,维护区间 [ l , r ] [l,r] [l,r]。注意 l l l 和 r r r 的初始值都为 0 0 0,并且对于遍历的每一个 i i i 都要满足 l ≤ i l\leq i l≤i,对于 r r r,我们可以让 r r r 尽量大。怎么样是不是有一种 KMP 的感觉了,找一个前后缀相同的区间,具体是这样的:
(由于这只蒟蒻不会自己画图所以他不得不从别人的博客里借图)
当前选到的 i i i 在 11 11 11 的位置上,于是我们进行一个类似于 KMP 的移位操作:
显然可以发现,我们的紫色框框里的两位是已经匹配上的。考虑以下情况:
当 i ≤ r i\leq r i≤r 时, s [ i , r ] s[i,r] s[i,r] 与 s [ i − l , r − l ] s[i-l,r-l] s[i−l,r−l] 是相等的,于是乎我们求此时的 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(i−l),r−i+1)。想一想,为什么是这样?首先,如果说 z ( i − l ) < r − i + 1 \operatorname{z}(i-l)<r-i+1 z(i−l)<r−i+1,那么匹配区间即黄色部分不会超出,于是乎我们就可以直接利用 z ( i ) = z ( i − l ) \operatorname{z}(i)=\operatorname{z}(i-l) z(i)=z(i−l);如果超出区间了话,先更新到 r − i + 1 r-i+1 r−i+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;
}