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++;
  • \(S_i \ne T_j\)

    • \(j=1\):i++; j++;
    • \(j>1\):j = nxt[j - 1] + 1;
posted @ 2023-01-31 18:06  Jiayn  阅读(94)  评论(0)    收藏  举报