给定一个模式串 \(S\),以及一个模板串 \(P\),所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 \(P\) 在模式串 \(S\) 中多次作为子串出现。
求出模板串 \(P\) 在模式串 \(S\) 中所有出现的位置的起始下标。
第一行输入整数 \(N\),表示字符串 \(P\) 的长度。
第二行输入字符串 \(P\)。
第三行输入整数 \(M\),表示字符串 \(S\) 的长度。
第四行输入字符串 \(S\)。
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
\(1≤N≤105\)
\(1≤M≤106\)
3 aba 5 ababa
0 2
KMP是一种字符串匹配算法,通常我们对于两个字符串匹配,一般使用两个循环嵌套进行暴力求解,很显然时间复杂度为\(O(m*n)\),是非常不乐观的,而KMP的时间复杂度可以优化到\(O(m+n)\)。
KMP算法由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)
我们称要匹配的字符串 s[]
为模式串, 类似地,称 p[]
为模板串,同时有一个 next[]
数组。
字符串前一部分 与 字符串后一部分 的一串字符相等时,我们分别称之为 匹配前缀 和 匹配后缀。
如上文所述,朴素算法我们使用两个循环暴力匹配,每次匹配失败,将模板串移动一位继续匹配但是这会造成许多无意义的匹配。为了解决这个问题,我们一定想要模板串能向后移动一段较长距离,这样就能减少时间消耗。那么这么最长距离怎么获得呢?
如上图,在匹配失败处的前一串字符,我们可以找到最长 匹配前缀 和 匹配后缀 ,它们是 ab
,匹配失败处前这段字符串我们已经和匹配串匹配了,意味着我们不需要再次匹配,于是我们移动整个模板串向后,使得 匹配前缀 对齐 匹配后缀 ,于是我们可以继续从 匹配前缀 的后一个字符继续开始匹配了,这段操作为我们节省了不少时间。
下面放一个KMP匹配过程,大家再体验一下。
我们对上述 移动模板串使得匹配前缀与匹配后缀对齐 这一行为进行量化,得到的是 NEXT
数组。
NEXT
数组的含义即:当前位置的字符不匹配时,我们应该返回到哪个位置继续进行匹配。
在这里举个例子,对于模板串 ababc
,我们构造得出以下 NEXT
数组:
注意:此处模板串数组下标从1开始
字符 | a | b | a | b | c |
---|---|---|---|---|---|
下标 | 1 | 2 | 3 | 4 | 5 |
返回位置 | 0 | 1 | 1 | 2 | 3 |
这里先贴出NEXT构造函数的代码,此处模板串数组下标从1开始
for (int i = 2, j = 0; i <= n; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; }
此处以对 next[n + 1]
求解举例,我们求解 next[n + 1]
,说明对于 next[1]...next[n]
我们肯定是已知的。
很显然我们有三种情况:
n + 1 == next[n]
这种情况很显然, next[n + 1] = next[n] + 1
, 因为每往后一位,最长匹配前后缀 最多只能加1。
n + 1 != next[n]
在图中,表现为 a != c
,这时候怎么办呢,我们会匹配 next[next[n]]
,此时匹配 a == a
,发现是匹配的,于是 next[n + 1] = next[next[n]] = 3
。
为什么会这样呢,我们看向上图,第一次匹配的时候我们发现 粉色 方框内的字符串是 \(n\) 之前的 最长匹配前后缀 ,而我们的 \(n+1\)不能进行情况1那样的对前一个 最长匹配前后缀 的延长,我们退而求其次,发现粉色框中,四个蓝色框是匹配的,因为粉色 = 粉色,而蓝色属于粉色内的 最长匹配前后缀, 所以四个蓝色也相等。我们便如此递推,直到第三种情况。
next[n + 1] = 1
如果n + 1前没有 最长匹配前后缀, 那我们只好重头匹配了,于是next[n + 1] = 1
。
KMP算法的精髓在于对已知信息的充分利用,这体现在待匹配串的匹配上面,更用于预处理时自己匹配自己上面,总而言之,KMP算法是非常值得学习的。
#include <iostream> using namespace std; const int N = 100010, M = 1000010; int n, m; int ne[N]; char s[M], p[N]; int main() { cin >> n >> p + 1 >> m >> s + 1; for (int i = 2, j = 0; i <= n; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; } for (int i = 1, j = 0; i <= m; i ++ ) { while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == n) { printf("%d ", i - n); j = ne[j]; } } return 0; }