浅谈KMP&扩展KMP
引入
考虑这样一个问题:
给出两个字符串,求在出现的所有位置
举例:对于ABACABAD
,ABA
,显然出现在位置
ABACABAD
ABA
ABACABAD
ABA
怎么求解?
很容易想到暴力解法:我们枚举的每一位作为的第一位,按位判断是否匹配,若不匹配则移动至下一位再次判断。
分析一下复杂度?
令长度为,枚举的每一位,按位查看,总复杂度很容易卡到,在很大的时候不够优。
所以出现了KMP算法。
Border
在介绍KMP算法之前,我们需要先了解一个字符串的Border
。其定义如下:
定义一个字符串 的 border 为 的一个非 本身的子串 ,满足 既是 的前缀,又是 的后缀。
举例:对于字符串ABABA
,它有个border
,分别是A
,ABA
KMP
KMP算法是一种改进的字符串匹配算法,由,和提出的,所以人们称它为KMP算法
考虑这样一组数据:
ABCABDABCABC
ABCABC
我们按位匹配:
ABCABDABCABC
ABCABC
ABCABC
ABCABC
ABCABC
......
不难发现前面几次移动使得第一位都无法匹配(),换句话说就是毫无意义的操作。我们同样看到,移动位之后,字符串变得很“匹配”:
ABCABDABCABC
ABCABC
ABCABDABCABC
ABCABC
此时前位都是相同的。
继续扩展?
每次移动的时候我们贪心地让的前几位和当前查看的字串的后几位尽量多的匹配。
换个说法?
每次移动的时候我们让的前缀和当前查看的字串的相同长度的后缀相同且长度最大。
前缀等于后缀?这不就是border
吗?
我们对于一个字符串定义一个next
数组,其中表示中最长的border
的长度。
于是我们便得到了KMP算法的核心思想:依据next
数组快速的“跳动”进行匹配从而大幅节省时间。
让我们手模一遍过程。
考虑数据ABAACABABCAC
,ABABC
先求出的next
数组:
从头开始匹配:
ABAABABABCAC
ABABC
发现匹配到第位的时候出现了问题,查看next
数组发现,这就意味着前位中第一位和最后一位相同,我们可以据此进行“跳跃”,直接把第一位移动到第三位再次匹配:
ABAABABABCAC
ABABC
此时发现匹配到第位的时候出现了问题,继续查看next
数组发现,前面不支持快速跳跃,所以只后移一位:
ABAABABABCAC
ABABC
此时发现匹配到第位的时候出现了问题,继续查看next
数组发现,所以我们后移:
ABAABABABCAC
ABABC
发现匹配成功,输出
一直这样做即可求出答案。
注意到在实现时跳跃完成后无需再次查看之前确定匹配的的前缀,只需向后查看,同时也是按位移动查看不会回头,故复杂度线性。
实现
KMP的思想很简单,但是实现有一定难度
- 考虑如何求解
next
数组。
暴力枚举求解next
数组的时间复杂度同样是无法接受的,所以我们dp
求解。
我们对于字符串,如果已经求出了,可以求出:
- 若
我们就可以直接把当前位加到border
里,即:
当前情况如图所示:
- 若
此时如果还是直接加入的话会出现问题,但是这也同时意味着一定小于
我们令,则此时问题等价于对于和求解。
同时,因为,所以的前位和的后位时一定相同的,我们要使加入新的后的border
尽可能大就可以考虑和是否相同。如果相同即可以取这个较劣但是最大合法的解作为。
如果不同?
那就递归继续找
由于此处较为复杂,若文字看不懂可以看图理解:
代码实现:
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);
}
由此便完成了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)
还是考虑一个问题:
给出字符串,求的每一个后缀与的最长公共前缀(LCP)的长度
暴力求解的复杂度是的,难以接受
我们对于一个字符串定义其函数,表示以为开头的后缀与的LCP
的长度。考虑通过dp
求得函数的值。
给出如下定义:
- 匹配段(Z-Box):对于,定义其匹配段为
我们看到当前位置,对于所有中的匹配段找到右端点最大的一个,不妨设为。
初始时
-
若
-
若
由定义可知此时我们直接令,理由可以看图理解。
-
若
直接令,然后向后枚举是否可以扩展。
-
-
若
暴力向后扩展
全部做完之后,查看是否大于,若是,则用更新
代码实现:
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;
}
注意到循环每次执行都会使右移至少一位,所以最多执行次,同时循环均线性,故总复杂度线性。
同时我们可以类似地对于两个字符串,以为基础求出和所有后缀的LCP
的长度,这就是扩展KMP
注意到这里的定义和我们不太一样,特殊处理即可
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步