浅谈KMP&扩展KMP
引入
考虑这样一个问题:
给出两个字符串\(S_1,S_2\),求\(S_2\)在\(S_1\)出现的所有位置
举例:对于\(S_1=\)ABACABAD,\(S_2=\)ABA,显然出现在位置\(1,5\)
ABACABAD
ABA
ABACABAD
ABA
怎么求解?
很容易想到暴力解法:我们枚举\(S_1\)的每一位作为\(S_2\)的第一位,按位判断是否匹配,若不匹配则移动\(S_2\)至下一位再次判断。
分析一下复杂度?
令\(S_1,S_2\)长度为\(n,m\),枚举\(S_1\)的每一位,按位查看,总复杂度很容易卡到\(O(nm)\),在\(n,m\)很大的时候不够优。
所以出现了KMP算法。
Border
在介绍KMP算法之前,我们需要先了解一个字符串的Border。其定义如下:
定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
举例:对于字符串ABABA,它有\(2\)个border,分别是A,ABA
KMP
KMP算法是一种改进的字符串匹配算法,由\(D.E.Knuth\),\(J.H.Morris\)和\(V.R.Pratt\)提出的,所以人们称它为KMP算法
考虑这样一组数据:
ABCABDABCABC
ABCABC
我们按位匹配:
ABCABDABCABC
ABCABC
ABCABC
ABCABC
ABCABC
......
不难发现前面几次移动使得第一位都无法匹配(\(A\not =B,A\not =C\)),换句话说就是毫无意义的操作。我们同样看到,移动\(3\)位之后,字符串变得很“匹配”:
ABCABDABCABC
ABCABC
\(\downarrow\)
ABCABDABCABC
ABCABC
此时前\(2\)位都是相同的。
继续扩展?
每次移动的时候我们贪心地让\(S_2\)的前几位和\(S_1\)当前查看的字串的后几位尽量多的匹配。
换个说法?
每次移动的时候我们让\(S_2\)的前缀和\(S_1\)当前查看的字串的相同长度的后缀相同且长度最大。
前缀等于后缀?这不就是border吗?
我们对于一个字符串\(S\)定义一个next数组,其中\(next_i\)表示\(S[0,i-1]\)中最长的border的长度。
于是我们便得到了KMP算法的核心思想:依据next数组快速的“跳动”进行匹配从而大幅节省时间。
让我们手模一遍过程。
考虑数据\(S_1=\)ABAACABABCAC,\(S_2=\)ABABC
先求出\(S_2\)的next数组:
\(next=\{0,0,0,1,2\}\)
从头开始匹配:
ABAABABABCAC
ABABC
发现匹配到第\(4\)位的时候出现了问题,查看next数组发现\(next_4=1\),这就意味着前\(3\)位中第一位和最后一位相同,我们可以据此进行“跳跃”,直接把第一位移动到第三位再次匹配:
ABAABABABCAC
ABABC
此时发现匹配到第\(2\)位的时候出现了问题,继续查看next数组发现\(next_2=0\),前面不支持快速跳跃,所以只后移一位:
ABAABABABCAC
ABABC
此时发现匹配到第\(5\)位的时候出现了问题,继续查看next数组发现\(next_5=2\),所以我们后移:
ABAABABABCAC
ABABC
发现匹配成功,输出
一直这样做即可求出答案。
注意到在实现时跳跃完成后无需再次查看之前确定匹配的\(S_2\)的前缀,只需向后查看,同时\(S_1\)也是按位移动查看不会回头,故复杂度线性。
实现
KMP的思想很简单,但是实现有一定难度
- 考虑如何求解
next数组。
暴力枚举求解next数组的时间复杂度同样是无法接受的,所以我们dp求解。
我们对于字符串\(S\),如果已经求出了\(next_i\),可以求出\(next_{i+1}\):
- 若\(S_{i+1}=S_{next_i+1}\)
我们就可以直接把当前位加到border里,即:
\(next_{i+1}=next_i+1\)
当前情况如图所示:

- 若\(S_{i+1}\not =S_{next_i+1}\)
此时如果还是直接加入的话会出现问题,但是这也同时意味着\(next_{i+1}\)一定小于\(next_i+1\)
我们令\(next_i=k\),则此时问题等价于对于\(S[0,k]\)和\(S[i-k+1,i+1]\)求解。
同时,因为\(next_i=k\),所以\(S[0,k]\)的前\(next_k\)位和\(S[i-k,i]\)的后\(next_k\)位时一定相同的,我们要使加入新的\(S_{i+1}\)后的border尽可能大就可以考虑\(S_{next_k+1}\)和\(S_{i+1}\)是否相同。如果相同即可以取这个较劣但是最大合法的解作为\(next_{i+1}\)。
如果不同?
那就递归继续找\(next\)
由于此处较为复杂,若文字看不懂可以看图理解:


代码实现:
int k = 0;
for (i = 1; i < lenp; ++i) {
while (k && pat[i] != pat[k]) k = nxt[k];
if (pat[i] == pat[k])
nxt[i + 1] = ++k;
else
nxt[i + 1] = 0;
}
- 考虑如何求解
我们在之前叙述思想时所用的“跳跃”“移动”等操作在代码中可以体现为指针的移动
我们使用两个指针,一个指针\(i\)指向\(S_1\),另一个指针\(j\)指向\(S_2\),每次比对\(i,j\)判断是否匹配
我们可以让\(i\)一直线性推进,当不匹配时将\(j\)前移使得整个\(S_2\)后移,从而达到我们的目的
如果当前\(j\)指向\(S_{k}\),那我们让\(j\)变为\(next_k\)即可实现转移。手模过程即可理解。
代码实现:
k = 0;
for (i = 0; i < lent; ++i) {
while (k && txt[i] != pat[k]) k = nxt[k];
if (txt[i] == pat[k]) ++k;
if (k == lenp) printf("%d\n", i - lenp + 2);
}
由此便完成了KMP算法
#include <bits/stdc++.h>
using namespace std;
char txt[1000005], pat[1000005];
int nxt[1000005];
signed main() {
scanf("%s%s", &txt, &pat);
register int i;
int lent = strlen(txt), lenp = strlen(pat);
int k = 0;
for (i = 1; i < lenp; ++i) {
while (k && pat[i] != pat[k]) k = nxt[k];
if (pat[i] == pat[k])
nxt[i + 1] = ++k;
else
nxt[i + 1] = 0;
}
k = 0;
for (i = 0; i < lent; ++i) {
while (k && txt[i] != pat[k]) k = nxt[k];
if (txt[i] == pat[k]) ++k;
if (k == lenp) printf("%d\n", i - lenp + 2);
}
for (i = 1; i <= lenp; ++i) printf("%d ", nxt[i]);
return 0;
}
Z函数(扩展KMP)
还是考虑一个问题:
给出字符串\(S\),求\(S\)的每一个后缀与\(S\)的最长公共前缀(LCP)的长度
暴力求解的复杂度是\(O(n^2)\)的,难以接受
我们对于一个字符串\(S\)定义其\(z\)函数,\(z(i)\)表示以\(i\)为开头的后缀与\(S\)的LCP的长度。考虑通过dp求得\(z\)函数的值。
给出如下定义:
- 匹配段(Z-Box):对于\(x\),定义其匹配段为\(S[x,x+z(x)-1]\)
我们看到当前位置\(S_i\),对于所有\(0\le x\le i\)中\(l\)的匹配段\(S[x,x+z(x)-1]\)找到右端点最大的一个,不妨设为\(S[l,r]\)。
初始时\(l=r=0\)
-
若\(i\le r\)
-
若\(z(i-l) < r-i+1\)
由定义可知\(S[i,r]=S[i-l,r-l],\)此时我们直接令\(z(i)=z(i-l)\),理由可以看图理解。

-
若\(z(i-l)\ge r-i+1\)
直接令\(z(i)=r-i+1\),然后向后枚举是否可以扩展。

-
-
若\(i>r\)
暴力向后扩展
全部做完之后,查看\(i+z(i)-1\)是否大于\(r\),若是,则用\(i,i+z(i)-1\)更新\(l,r\)
代码实现:
register int i;
int l = 0, r = 0;
for (i = 1; i < n; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0, r - i + 1);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
注意到\(while\)循环每次执行都会使\(r\)右移至少一位,所以最多执行\(n\)次,同时\(for\)循环均线性,故总复杂度线性。
同时我们可以类似地对于两个字符串\(S,T\),以\(S\)为基础求出\(T\)和\(S\)所有后缀的LCP的长度,这就是扩展KMP
注意到这里\(z(0)\)的定义和我们不太一样,特殊处理即可
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 2e7 + 5;
int n, m, z[MAXN], p[MAXN];
char s1[MAXN], s2[MAXN];
inline void Z(char *s, int len) {
register int i, j = 0;
int l = 0, r = 0;
z[0] = len;
while (j + 1 < len && s[j] == s[j + 1]) ++j;
z[1] = j;
for (i = 2; i < len; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0ll, r - i + 1);
while (i + z[i] < len && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
inline void exkmp(char *s1, int len1, char *s2, int len2) {
register int i, j = 0;
while (j < len1 && j < len2 && s1[j] == s2[j]) ++j;
p[0] = j;
Z(s2, len2);
int l = 0, r = 0;
for (i = 1; i < len1; ++i) {
if (i <= r && z[i - l] < r - i + 1) {
p[i] = z[i - l];
} else {
p[i] = max(0ll, r - i + 1);
while (i + p[i] < len1 && p[i] < len2 && s2[p[i]] == s1[i + p[i]])
++p[i];
}
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
}
inline int solve(int *a, int len) {
int ans = 0ll;
register int i;
for (i = 0; i < len; ++i) ans ^= 1ll * (i + 1) * (a[i] + 1);
return ans;
}
signed main() {
scanf("%s%s", s1, s2);
n = strlen(s1), m = strlen(s2);
exkmp(s1, n, s2, m);
printf("%lld\n%lld\n", solve(z, m), solve(p, n));
return 0;
}

浙公网安备 33010602011771号