怎么突然从图论跳到字符串了。。。
UPDATE 2021 - 08 - 23
KMP 练习题:
P3435 [POI2006]OKR-Periods of Words
题解博客:P3426 [POI2005]SZA-Template (题记)
P4391 [BOI2009]Radio Transmission 无线传输
定义
KMP 算法是用来处理字符串匹配问题的。
话说这个算法名还是提出它的三个人的姓首字母拼成的。。。
基本问题:
给你两个字符串,需要你回答,B 串是否是 A 串的子串。
我们称 A 串为母(主)串,用来匹配的 B 串为模式串。
暴力的时间复杂度是 O ( n m ) O(nm) O(nm),但是 KMP 的时间复杂度是 O ( n ) O(n) O(n) (假设 m ≤ n m \leq n m≤n)。
代码很短,但是思路较为复杂。
算法流程
先来模拟一下暴力的流程:
用两个指针分别指向母串和模式串,按位比较,
如果失配(即 a [ i ] ! = b [ j ] a[i] != b[j] a[i]!=b[j]),
那么我们的 i 回到刚才起始位加一的地方,j 回到 1(重新再匹配一次)。
这样很明显会超时,那么我们就来优化重新匹配所需的时间(在已失配的情况下)。
ps:接下来所有字符串默认下标从 1 开始,写代码时直接 scanf("%s", a + 1);
(a 为 char
类型的)即可。
KMP 的主要思想就是:
失配后 j(指向模式串的指针)不回到 1,而是回到某个位置,
使得能少匹配很多不必要的字符。
我们不对 i(指向母串的指针)做改变,只对 j 的指向位置进行优化。
因此,不论失配与否,i 都加一,跳到母串的下一个字符(即我们接下来的操作都是针对模式串的)。
那 j 要怎么跳才能优化次数呢?
先给出一个模式串:
a b c d a b d。
假若我们之前和母串都匹配成功,直到我们匹配到第七个字符——d。
注意:
j 此时是指向 6 的,我们每次匹配都是对 j + 1 j+1 j+1 进行匹配,
也就是每次指向将要匹配的字符——这样若失配了,我们就可以直接跳到 k m p j kmp_j kmpj( j + 1 j+1 j+1 失配后指向的下标)。
言归正传,我们在匹配到第七个时失配了:
-
暴力:j 重新指向 1;
-
KMP:j 指向 3。
暴力不说了, KMP 为什么要指向 3?
若前 1 到 6 个都匹配成功了,我们要重新开始匹配了,
可以看见, b j = b 2 b_j = b_{2} bj=b2 且 b j − 1 = b 1 b_{j-1} = b_{1} bj−1=b1 ( j ← 6 j \gets 6 j←6、 i ← 7 i \gets 7 i←7),
我们就抽象地想象把模式串整体移位,让 1 对准 j − 1 j-1 j−1,
后面依次和母串对齐,继续匹配。
这样做,可以发现,我们倒数二三位既然已经匹配成功了,
那么我们就不需要再次匹配,接下来让 c 和 a i a_{i} ai 匹配即可。
这样就可以保证,a 串不需要重复匹配,每次移动 j 即可。
通过上述举例推论,发现:
每次若 j + 1 j+1 j+1 失配了,我们就跳到除去 j + 1 j+1 j+1 位最长相同的前缀后缀的长度加一的位置。
(若前缀后缀相同,即不用再匹配前缀了,直接匹配不同的即可。)
举个例子(下标从 1 开始):
模式串:a b a a b a c;
kmp 数组:0,0,1,1,2,3,0。
代码实现
kmp 数组
kmp 数组存储每次 j + 1 j+1 j+1 失配后要跳到哪里(包含 j 的最长前后缀长度)。
求 kmp 的过程就是模式串自己和自己匹配的过程。
这个的过程就和上述的 KMP 算法过程相同了。
(模式串下标从 1 开始。)
第一位不需要寻找 kmp 值,我们循环从 2 到 lenb——每次都是计算 i 的 kmp 值。
一样地,我们有一个下标 j 指向“模式串”(此时母串和模式串都是 b 串)。
然后每次当 j + 1 j+1 j+1 失配后,直接跳到 k m p j kmp_j kmpj 即可,
后面匹配还是拿 j + 1 j+1 j+1 匹配。
最后如果匹配成功,即 k m p i + 1 ← + + j kmp_{i+1} \gets ++j kmpi+1←++j。
代码:
for (int i = 2, j = 0; i <= lenb; i++)
{
while (j and b[i] != b[j + 1]) j = kmp[j];
if (b[i] == b[j + 1]) j++;
kmp[i] = j;
}
求模式串于母串的位置
和求 kmp 数组相似,它是拿母串和模式串匹配(上述的是模式串自我匹配)。
唯一需要注意的是:
-
从第 1 位匹配到第 lena 位;
-
若匹配成功,记得 j 需要回到 k m p j kmp_j kmpj,继续匹配(具体因题目要求而定)。
代码:
int j = 0;
for (int i = 1; i <= lena; i++)
{
while (j and b[j + 1] != a[i]) j = kmp[j];
if (b[j + 1] == a[i]) j++;
if (j == lenb)
{
printf ("%d\n", i - lenb + 1);
j = kmp[j];
}
}
总体来说,KMP 的思维难度确实较大,
但是可以通过多刷 KMP 的题,找找关于 kmp 数组的感觉。
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000010;
int kmp[maxn];
int lena, lenb;
char a[maxn], b[maxn];
int main ()
{
scanf ("%s %s", a + 1, b + 1);
lena = strlen (a + 1), lenb = strlen (b + 1);
for (int i = 2, j = 0; i <= lenb; i++)
{
while (j and b[i] != b[j + 1]) j = kmp[j];
if (b[i] == b[j + 1]) j++;
kmp[i] = j;
}
int j = 0;
for (int i = 1; i <= lena; i++)
{
while (j and b[j + 1] != a[i]) j = kmp[j];
if (b[j + 1] == a[i]) j++;
if (j == lenb)
{
printf ("%d\n", i - lenb + 1);
j = kmp[j];
}
}
for (int i = 1; i <= lenb; i++) printf ("%d ", kmp[i]);
return 0;
}
—— E n d End End——