KMP 算法
写作时间:2023/1/30/16:33
1. KMP 算法是什么
-
问题:
给出两个字符串 \(s_1\) 和 \(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\) 在 \(s_1\) 中出现了,其出现位置为 \(l\)。
定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
现在请你求出 \(s_2\) 在 \(s_1\) 中所有出现的位置。对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。 -
KMP 可以在 \(O(N+M)\) 的时间内解决如上的字符串匹配的问题。(\(N, M\) 分别是两个字符串的长度)
2. 关于 nxt 数组
2.1 定义
对于字符串 \(S\), \(nxt_i\) 表示 :\(S[1...i]\) 中最长的相等真前缀与真后缀的长度。(根据定义即可发现,\(nxt_i<i\))
例如,对于 \(S=ababac\), \(nxt_1=0, nxt_2=0, nxt_3=1, nxt_4=2, nxt_5=3, nxt6=0\)。(ps. \(nxt\) 数组其实对应的就是 \(border\))
2.2 引理※
-
引理 1:\(nxt_i\)(对于 \(nxt_i>1\)) 所对应的相等真前缀与真后缀是由 \(S[1...i-1]\) 中的一个相等真前缀与真后缀扩展而来的,当然,不一定是由 \(S[1...i-1]\) 中那个最长的相等真前缀与真后缀扩展而来的。
-
引理 2:\(nxt_i\), \(nxt_{nxt_i}\), \(nxt_{nxt_{nxt_i}}\)....,分别对应 \(S[1...i]\) 中所有相等真前缀与真后缀的依次递减的长度。※
2.2 nxt[i] 怎么求?
首先,假设我们已知 \(nxt_{1}..nxt_{i-1}\)。
根据引理 1, 有这样一个思路:枚举所有 \(S[1...i-1]\) 的相等真前缀与真后缀,枚举顺序是按长度依次递减。设当前枚举到的相等真前后缀的长度是 \(l\), 若发现可以扩展,即 \(S_{l+1}=S_i\), 则 \(l\) 就是 \(nxt_i\) 的答案。
至于怎么枚举所有 \(S[1...i-1]\) 的相等真前后缀,根据引理 2, 可知,\(nxt_{i-1}, nxt_{nxt_{i-1}}\)... 构成了所有 \(S[1...i-1]\) 的相等真前后缀长度。又因为 \(nxt_i \le i-1\) 且我们已知 \(nxt_{1}...nxt_{i-1}\), 所以我们知道 \(nxt_{i-1}, nxt_{nxt_{i-1}}\)...
求 nxt 数组的代码
void getnxt(){
for(int i = 1; i <= n; i++) nxt[i] = 0;
nxt[0] = -1;
for(int i = 2, j; i <= n; i++){
j = nxt[i - 1];
while(a[j + 1] != a[i] && j != -1) j = nxt[j];
if(j != -1) nxt[i] = j + 1;
}
}
有了 \(nxt\) 数组,接下来就是利用 \(nxt\) 数组解决匹配问题。
(先说明一下,后面提到的 \(S\) 是文本串,\(T\) 是模式串,\(len1, len2\) 分别是 \(S\) 与 \(T\) 的长度)
3. KMP 算法流程
现在有两个指针 \(i\),\(j\), 分别表示 当前要匹配 \(S_i\), \(T_j\)。接下来,分类讨论。
-
\(S_i=T_j\)
- \(j=len2\):记录答案,
j = nxt[j] + 1; i++;(中间直接跳过※) - \(j \ne len2\):
i++; j++;
- \(j=len2\):记录答案,
-
\(S_i \ne T_j\)
- \(j=1\):
i++; j++; - \(j>1\):
j = nxt[j - 1] + 1;
- \(j=1\):

浙公网安备 33010602011771号