KMP

引入

kmp 需要处理的问题是“字符串匹配问题”,具体问题如下:
【模板】KMP 字符串匹配AC 代码提交记录

给出两个字符串 \(s_1\)\(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\)\(s_1\) 中出现了,其出现位置为 \(l\)
现在请你求出 \(s_2\)\(s_1\) 中所有出现的位置。

首先,这个问题有一个很显然的暴力做法,就是枚举 \(s_1\) 中的所有位置,然后向后枚举 \(|s_2|\) 位直接尝试匹配。
s1=ababaabaabacs2=abaabac,暴力匹配的流程如下:

编号 a b a b a a b a a b a c 匹配结果
1 a b a a b a c 前三位匹配成功,第四位失配
2 a b a a b a c 第一位失配
3 a b a a b a c 前六位匹配成功,第七位失配
4 a b a a b a c 第一位失配
5 a b a a b a c 第一位匹配成功,第二位失配
6 a b a a b a c 匹配成功

这个算法时间复杂度 \(O(|s_1||s_2|)\),不够优秀。如果考虑优化,我们会发现:

  • 两个字符串 \(s_1\)\(s_2\) 都至少遍历一遍。
  • 每次失配之后,都是从头枚举,想要优化就必须从之前的失配中吸取经验。

前缀函数

我们先来看一个东西:

定义一个字符串 \(s\) 的 border 为 \(s\) 的一个\(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。

这是 P3375 中提到的一个相关定义,接下来将详细解析所有与之相关的字符串定义。

  • 子串】:字符串 \(s\)连续的一部分,具体而言:设 \(1\leq i\leq j\leq s.length()\),字符串 \(s\) 的第 \(i\) 位到第 \(j\) 位,即 \(s[i],s[i+1],\cdots,s[j]\) 所形成的一个字符串,就称为子串,用 \(s[i...j]\) 表示。
  • 真子串】:很好理解,\(s\) 的所有子串中,只有 \(s\) 本身不是 \(s\) 的真子串,毕竟 \(s\) 就是它本身嘛,算一个“假的”子串。
  • 前缀】:这个也很好理解,字符串 \(s\) 头上的一部分就是前缀,即 \(s[1...i]\)
  • 真前缀】:和真子串一样,所有前缀中只有 \(s\) 本身不是真前缀。
  • 与之相对应的,还有【后缀】、【真后缀】这两个概念,此处就不再赘述。

接下来我们举一个例子,来介绍题干中 border 的概念。
我们取 s=ababa,很显然,a 既是 \(s\) 的一个真前缀,也是 \(s\) 的一个真后缀,于是 a 就是符合题干的一个 border
符合条件的 border 还有 aba,于是题干中所求的【最长 border 】的长度也就是 \(3\)

对于字符串 \(s\),其【前缀函数\(\pi[i]\) 定义为:子串 \(s[1...i]\) 的最长 border 长度。也就是说,题干中所要求出的【每个前缀 \(s'\) 的最长 border \(t'\) 的长度】就是字符串 \(s\) 的所有前缀函数值。

比如,我们取 s=abaabac,其前缀函数的值计算如下:

\(i\) \(s[i]\) \(\pi[i]\) 最长 border
1 a 0 不存在真前缀和真后缀
2 b 0 ab 不一样
3 a 1 a
4 a 1 a
5 b 2 ab
6 a 3 aba
7 c 0 ac 不一样

前缀函数的计算方法我们将在后文介绍。

kmp 与前缀函数

上一节介绍了前缀函数的概念,那么它和字符串匹配问题有什么关系呢?
设已知字符串 \(s_1\)\(s_2\),其中 \(s_2\) 的长度为 \(sz\)
如果 \(s_2\)\(s_1\) 中出现了,那么必然存在一个位置 \(i\),满足 \(s_2 = s_1[(i-sz+1)...i]\),即子串 \(s_1[1...i]\) 长度为 \(sz\) 的后缀恰好和 \(s_1\) 相等。

  • 比如我们可以取 s1=ababaabaabacs2=abaabac。因为子串 \(s_1[1...12]\) 有一个长度为 \(7\) 的后缀 abaabac,恰好等于 \(s_2\),所以我们就可以判断 \(s_2\)\(s_1\) 中出现了。

我们可以令 \(T=s_2+s_1\),即将 \(s_1\) 直接接在 \(s_2\) 后面。
那么字符串匹配就转换为了:若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\)border,那么就匹配成功了。
如果我们能找到一种合理的方式计算前缀函数,并且找到所有长度的 border,那么这个问题就得到了解决。

如何计算前缀函数?

对于字符串 \(s\),如果我们想计算其前缀函数,一种暴力的方法是:一重循环枚举子串 \(s[1...i]\),一重循环枚举子串的真前缀 \(s[1...j]\),然后和对应的真后缀比较,时间复杂度 \(O(|s|^3)\)
我们可以考虑递推的方式来优化这个过程。
s=abcxabcwabcxabcx,假设我们已经计算出了前 14 位的前缀函数 \(\pi[i]\)

i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
\(s[i]\) a b c x a b c w a b c x a b c x
\(\pi[i]\) 0 0 0 0 1 2 3 0 1 2 3 4 5 6
  • 在考虑第 15 位的时候,我们发现 \(s[1...14]\) 有一个长度为 6 的 border,即 \(s[1...6]=s[9...14]\),并且 \(s[7]=s[15]\),于是这个 border 可以再次延伸一格,变为 abcxabc,即 \(\pi[15]=\pi[14]+1=7\)
  • 在考虑第 16 位的时候,我们依然想按照上述的方法,比较 \(s[\ \pi[i-1]+1\ ]\)\(s[i]\)
    • 结果发现 \(s[8]=w\neq s[16]=x\)。也就是说,abcxabcx 不是 \(s[1...16]\) 的一个 border
    • 此时如果按照暴力的做法,应该枚举 bcxabcx 是不是一个 border。但其实,我们还可以再利用一次 \(\pi[]\)
    • 仅仅通过 \(\pi[15]\),我们就可以快速知道 \(s[1...7]=s[9...15]\)。如果我们此时取 \(s[1...\pi[7]]\),即 abc,由前缀函数的定义我们可以知道:abc 既是 \(s[1...7]\) 的真前缀,也是 \(s[1...7]\) 的真后缀,既是 \(s[9...15]\) 的真前缀,也是 \(s[9...15]\) 的真后缀。于是我们就找到了一个长度第二大的 border
    • 此时再比较 \(s[\pi[7]+1]\)\(s[16]\) 即可,循环执行此流程, 即可求出 \(\pi[]\)

接下来展示求 \(\pi[i]\) 的代码,代码中变量名采用 nxt[i]

int nxt[MAXN];

// I.N.
string s2; cin >> s2;
int len2 = s2.length();
s2 = " " + s2;

// 初值
nxt[1] = 0;
for (int i=2, j=0; i <= len2; ++i) {
	// 如果s2[i]==s2[j+1],那么原来的border就可以延伸为s2[1...j+1]
	// 否则,找到一个长度更小的border,看看是不是能延伸
	while (j && s2[i] != s2[j+1]) { j = nxt[j]; }
	// 原来的border延伸一位
	if (s2[i] == s2[j+1]) { ++j; }
	// 记录求出的前缀函数值
	nxt[i] = j;
}

kmp 的流程

在计算了前缀函数,并且找到所有长度的 border 之后,字符串匹配问题也就得到了解决。
按照前文所述,将原问题转换为:【若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\)border,那么就匹配成功了。
代码如下:

void kmp()
{
	for (int i=1, j=0; i <= len1; ++i) {
		// 假想一个拼接的字符串T=s2+s1,接下来的两行代码和求前缀函数就一样了
		while (j && s1[i]!=s2[j+1]) { j = nxt[j]; }
		if (s1[i] == s2[j+1]) { ++j; }
		// 因为没有真的拼接,j最大也就等于s2的长度,此时就是匹配上了,输出答案
		if (j == len2) { cout << i-len2+1 << endl; }
	}
}

前缀函数与字符串的周期

UVA1328 PeriodAC 代码提交记录
设循环节的长度为 \(k\),那么 \(s[1...i]\) 有长度为 k 的循环节的充要条件为:\(s[1...i-k+1] = s[1+k...i]\),并且 \(i\mod k=0\)
即:前后各自长度为 \(i-k+1\) 的真前后缀相等,并且 \(s[1...i]\) 恰好能容下整数个循环节。
当枚举到位置 \(i\) 的时候,求出它的 \(nxt[i]\)
此时 \(s[1 ... nxt[i]] = s[i-nxt[i]+1 ... i]\) 是已经确定的。
并且由于 \(nxt[i]\) 是最长相同前后缀的长度,此时的循环节必然是最小的那一个。
只需要判断是否满足 \(i\mod (i-nxt[i])=0\) 即可。
如果满足,此时周期串的长度为 \(i\),循环节的长度为 \(i-nxt[i]\),循环节的个数为 \(\displaystyle\frac{i}{(i-nxt[i])}\)
参考代码:

#include <bits/stdc++.h>

using namespace std;

const int MAXN=1e6+5;
int t, n, nxt[MAXN];
string s;

int main()
{
	while ( (cin>>n) && (n!=0) ) {
		cin >> s; s=" "+s;
		cout << "Test case #" << ++t << endl;
		nxt[1]=0;
		for (int i=2, j=0; i <= n; ++i) {
			while (j && s[i]!=s[j+1]) { j=nxt[j]; }
			if (s[i] == s[j+1]) { ++j; }
			nxt[i] = j;
			if ( (nxt[i]!=0) && (i%(i-nxt[i])==0)) { cout << i << " " << i/(i-nxt[i]) << endl; }
		}
		cout << endl;
	}
	return 0;
}
posted @ 2023-08-21 20:56  LittleDrinks  阅读(10)  评论(0编辑  收藏  举报