扩展 KMP
基本概念
扩展 \(KMP\) 算法是一种基于 \(KMP\) 算法思想的字符串算法,通常用于解决给出主串 \(S\) 和模式串 \(T\),求出 \(S\) 的每一个后缀与 \(T\) 的最长前缀长度(\(LCP\))。扩展 \(KMP\) 算法的核心思想还是利用已经计算过的信息减少字符串的暴力匹配长度,从而优化时间复杂度到 \(O(|S| + |T|)\)。类似地,扩展 \(KMP\) 算法维护模式串自己与自己匹配以及模式串和主串匹配的情况,并通过前者来维护后者。
当下标从 \(0\) 开始时,用 \(T_{i, j}\) 表示模式串 \(T\) 从下标 \(i\) 到 \(j\) 的子串。\(|T|\) 表示字符串 \(|T|\) 的长度。定义 \(nxt_i\) 表示模式串 \(T\) 的后缀 \(T_{i, |T|}\) 与模式串 \(T\) 自身的 \(LCP\) 长度,\(ext_i\) 表示主串 \(S\) 的后缀 \(S_{i, |S|}\) 与模式串 \(T\) 的 \(LCP\) 长度。扩展 \(KMP\) 算法就是利用 \(KMP\) 算法的思想求出 \(nxt_i\),从而求出 \(ext_i\) 解决问题的。
因为有字符串 \(s\) 的 \(z\) 函数表示 \(s\) 和 \(s\) 的每一个后缀的 \(LCP\) 长度,因此扩展 \(KMP\) 算法又被称为 \(z\) 函数或者 \(z\) 算法。
算法思想
扩展 \(KMP\) 算法实际上是通过将字符串划分成相同的若干段来确定一部分匹配成功的子串,从而减少匹配长度的。假设我们有一模式串 \(T\),考虑求出模式串 \(T\) 的 \(nxt\)。不妨用一个长条来表示模式串 \(T\),长条中相同颜色的小段表示其对应的子串完全相同(包括长度、字符、性质等完全相同)。当然,颜色会存在覆盖,因此新的颜色是建立在上文的基础上的。
那么初始时字符串 \(T\) 应该是这个样子:
假设我们现在已经求出了 \(nxt_{0}\) 到 \(nxt_{i - 1}\),需要求出 \(nxt_{i}\)。设 \(\forall 0 \leq j \leq i - 1, p = \operatorname{argmax} \{j + nxt_j - 1\}, r = \max \{j + nxt_j - 1\}\)。我们在图中标出 \(i, p, j\) 的位置。我们先考虑 \(i < r\) 的情况,如下图:
根据 \(nxt_p\) 的定义,模式串 \(T\) 的前缀 \(T_{0, nxt_p - 1}\) 应该和子串 \(T_{p, r}\) 完全相同,我们在图中标出前缀 \(T_{0, nxt_p - 1}\),如下图:
我们在图中把 \(T_{i, r}\) 用蓝色标注,那么因为 \(T_{0, nxt_p - 1} = T_{p, r}\),所以它们相同位置的子区间也完全相同,因此 \(T_{0, nxt_p - 1}\) 的后缀 \(T_{i - p, nxt_p - 1}\) 应该也是蓝色,如下图:
不妨设 \(k = nxt_{i - p}\),假设 \(i + k - 1 \leq r\),我们发现 \(T_{0, k - 1} = T_{i - p, i - p + k - 1} = T_{i, i + k - 1}\)。也就是说,因为 \(T_{i - p, nxt_p - 1} = T_{i, r}\) 并且 \(T_{i - p + k} \neq T_k\),即 \(nxt_{i - p}\) 不可能超过下标 \(nxt_p - 1\),因此 \(nxt_i\) 也不可能超过下标 \(r\),此时 \(nxt_i = nxt_{i - p}\)。如下图:
反之,若 \(i \geq r\) 或 \(i + k - 1 \geq r\),说明可能还有未被匹配的部分,从下标 \(r, r - i\) 开始逐位比较匹配。
求 \(ext_i\) 的思路和求 \(nxt_i\) 的思路类似。假设我们已知 \(ext_0\) 到 \(ext_{i - 1}\),现在要求 \(ext_i\)。
依然设 \(\forall 0 \leq j \leq i - 1, p = \operatorname{argmax} \{j + ext_j - 1\}, r = \max\{j + ext_j - 1\}\)。在图中用红色标出:
在两图中分别标出 \(i\) 和 \(i - p\),用蓝色标出:
然后标出 \(nxt_{i - p}\),划分出三段绿色:
此时若 \(i + nxt_{i - p} - 1 \leq r\),说明 \(ext_{i} = nxt_{i - p}\)。反之从 \(ext{p}, r + 1\) 开始暴力匹配。
参考代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 2e7 + 5;
int n, m;
int nxt[maxn], ext[maxn];
char s[maxn], t[maxn];
void get_nxt()
{
int j = 0, p = 1, q;
nxt[0] = m;
while (j + 1 < m && t[j] == t[j + 1])
j++;
nxt[1] = j;
for (int i = 2; i < m; i++)
{
q = p + nxt[p] - 1;
if (i + nxt[i - p] <= q)
nxt[i] = nxt[i - p];
else
{
j = max(q - i + 1, 0);
while (i + j < m && t[i + j] == t[j])
j++;
nxt[i] = j, p = i;
}
}
}
void get_ext()
{
int j = 0, p = 0, q;
while (j < n && j < m && s[j] == t[j])
j++;
ext[0] = j;
for (int i = 1; i < n; i++)
{
q = p + ext[p] - 1;
if (i + nxt[i - p] <= q)
ext[i] = nxt[i - p];
else
{
j = max(q - i + 1, 0);
while (i + j < n && j < m && s[i + j] == t[j])
j++;
ext[i] = j, p = i;
}
}
}
int main()
{
long long z, p;
z = p = 0;
scanf("%s", s);
scanf("%s", t);
n = strlen(s);
m = strlen(t);
get_nxt();
get_ext();
for (int i = 0; i < n; i++)
p ^= (1LL * (i + 1) * (ext[i] + 1));
for (int i = 0; i < m; i++)
z ^= (1LL * (i + 1) * (nxt[i] + 1));
printf("%lld\n%lld\n", z, p);
return 0;
}