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\)连续的一部分,具体而言:设 \(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\) 本身不是真前缀。
  • 与之相对应的,还有【后缀】、【真后缀】这两个概念,此处就不再赘述。

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

这是 P3375 中提到的一个相关定义。接下来我们举一个例子,来介绍题干中 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 不一样

如何计算前缀函数

对于字符串 \(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]\) 的代码。

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

根据 \(\pi[i]\) 的定义,\(\pi[i]-\pi[i-1]\leq1\),求前缀函数的过程可以视作不断 \(+1\) 的过程,\(\pi[i]\leq |s|\)

而一旦失配,前方累计的长度会经历若干次“释放”,此时复杂度最劣的情况下,while (j && s[j]!=s[i]) { j = pi[j-1]; } 的循环可以视作不断地 \(-1\),复杂度最劣为 \(O(\max\{\pi[i]\})=O(|s|)\)

因此,这个算法可以在线性的时间内求解前缀函数。

前缀函数与 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 + \texttt{\#} + s_1\),也就是将 \(s_1\) 直接接在 \(s_2\) 后面。那么字符串匹配就转换为了:若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\)border,那么就匹配成功了。

在计算了前缀函数,并且找到所有长度的 border 之后,字符串匹配问题也就得到了解决。代码如下:

void kmp()
{
    string s, t;
    cin >> s >> t;
    int n = s.length(), m = t.length();
    vector<int> pi = prefixFunction(t+"#"+s);
    for (int i = m+1; i <= n+m; ++i) {
        if (pi[i] == m) { cout << i-2*m+1 << "\n"; }
    }
    for (int i = 0; i < m; ++i) { cout << pi[i] << " "; }
}

UVA1328 Period

设循环节的长度为 \(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\) 的时候,求出它的 \(\pi[i]\)。此时 \(s[1 ... \pi[i]] = s[i-\pi[i]+1 ... i]\) 是已经确定的。并且由于 \(\pi[i]\) 是最长相同前后缀的长度,此时的循环节必然是最小的那一个。

只需要判断是否满足 \(i\mod (i-\pi[i])=0\) 即可。如果满足,此时周期串的长度为 \(i\),循环节的长度为 \(i-\pi[i]\),循环节的个数为 \(\displaystyle\frac{i}{(i-\pi[i])}\)

AC 代码

#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  阅读(34)  评论(0)    收藏  举报