Zth's Blog

记录学习路上的点滴

0%

Kmp

Kmp算法(源于《算法竞赛进阶指南》)

前言

Kmp算法, 又叫字符串模式匹配算法, 能在线性的时间内判断出字符串$A$是否是字符串$B$的字串, 并求出其出现的位置

Kmp算法

Kmp算法分两步

  1. 对字符串$A$进行自我匹配, 求出一个数组$next[i]$表示$A$中以$i$结尾的非前缀字串与$A$的前缀能匹配的最长长度, 即

    特别地, 如果不存在这样的$j$, $next[i] = 0$

  2. 将$A$和$B$进行匹配, 求出一个数组$f[i]$表示$B$中以$i$结尾的字串能与$A$的前缀匹配的最长的长度, 即

如何快速计算$next$数组

我们递推地计算$next$数组, 假设$next[1, i - 1]$已经计算完毕, 我们现在要计算$next[i]$, 我们要求最大的$j$, 使得$j < i, A[i - j + 1, i] = A[1, j]$, 忽略最大这个条件, 我们称满足后面两个条件的$j$称为$next[i]$的候选项

引理

若$j_0$是$next[i]$的一个候选项, 那么小于$j_0$的最大的$next[i]$的候选项是$next[j_0]$

证明

如图所示, 由$next$数组的定义可知红色的区间一定是相同的, 我们现在要在以$i$结尾的红色区间上找一段最长的后缀区间(绿色)与$A$的前缀相同, 由于两个红色区间完全相同, 这个操作可以转移到以$j0$结尾的红色区间上进行, 那我们就是要找一个最大的$j_1$, 满足$j_1 < j_0, A[j_0 - j_1 + 1, j_0] = A[1, j_1]$, 这恰好就是$next[j_0]$的定义!!!

利用引理计算$next$数组

根据引理, 我们计算完$next[i - 1]$时, $next[i - 1]$的所有候选项从大到小依次是$next[i - 1], next[next[i - 1]], …$, 如果一个整数$j$是$next[i]$的候选项, 那么$j - 1$必然是$next[i - 1]$的候选项, 所以在计算$next[i]$时, 我们只需要将$next[i - 1] + 1, next[next[i - 1]] + 1, …$当作$next[i]$的候选项就可以了, 我们依次从大到小枚举$next[i - 1]$的候选项, 直到遇到一个候选项$j, A[j + 1] = A[i]$, 那么有$next[i] = j + 1$, 代码中由于$j$对于$i$是全局变量, 所以这个时候应该$j ++$

$f$数组

与$next$数组类似

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstdio>
#include <cstring>

int n, m;
char a[1000005], b[100005];
int next[100005], f[100005];

void Kmp()
{
next[1] = 0;
for(int i = 2, j = 0; i <= n; i ++)
{
while(j > 0 && a[j + 1] != a[i]) j = next[j]; // 找到第一个a[j + 1] = a[i]的位置作为next[i]

if(a[i] == a[j + 1]) j ++; // j相对所有i为全局变量, 所以要 ++

next[i] = j;
}

for(int i = 1, j = 0; i <= m; i ++)
{
while(j > 0 && (j == n || a[j + 1] != b[i])) j = next[j]; // 多一个 j == n 的判断

if(b[i] == a[j + 1]) j ++;

f[i] = j;

// if(f[i] == n) 此时表示a在b中出现了一次
}
}

int main()
{
scanf("%s%s", a + 1, b + 1);

n = strlen(a + 1), m = strlen(b + 1);

Kmp();

return 0;
}

时间复杂度

在计算$next$数组时, $j$最多加了$n$次

whille时, $j$可能递减多次, 但是由于$j$一直非负, 所以$j$减小的幅度不会超过$j$增加的幅度

计算$f$时同理

时间复杂度$O(n + m)$