【题解】CF432D - Prefixes and Suffixes
题目大意
给定一个长度为 \(n\) 的模式串 \(S\)。定义 完美子串 表示 \(S\) 的子串中既是 \(S\) 的前缀又是 \(S\) 的后缀的子串。对于给出的模式串 \(S\),试求所有可能的完美子串以及它们出现的位置。输出每个完美子串对应前缀的尾下标以及它在 \(S\) 中出现的次数,按尾下标升序输出。
\(1 \leq n \leq 10^5\)
解题思路
正常来说不会想到用扩展 \(KMP\),但是因为扩展 \(KMP\) 做法代码量比 \(KMP\) 做法略小,因此本文着重讲解扩展 \(KMP\) 解法并赋上代码。扩展 \(KMP\) 做法的字符串下标从 \(0\) 开始。另外的做法还有:
- 正解:\(KMP + dp\)
- 正解:\(SAM\)(\(+ AC\) 自动机)
- 乱搞:\(KMP + AC\) 自动机
从题目中 既是前缀又是后缀 可以联想到用扩展 \(KMP\) 来维护。显然如果以 \(i\) 为左端点的后缀是 \(S\) 的完美子串,那么一定有 \(nxt_i = n - i\) 。我们可以很轻松地判断出某个后缀是否是完美子串,难点在于维护完美子串的出现次数。
最暴力的做法是对于每一个完美子串都 \(KMP\) 一次,时间复杂度为 \(O(n ^ 2)\)。优化的思路是把若干个完美子串用 \(AC\) 自动机来维护,时间复杂度是正确的,但是代码量比正常的 \(KMP\) 做法还大,并且很容易爆空间。我们考虑在线性时间复杂度内求出每一个完美子串的出现次数,发现似乎并不可做。因此我们考虑 构造 一个新的字符串,使得我们维护起来更加方便。
不妨令新串 \(S^{\prime} = S|S\),设 \(m = |S^{\prime}|\),下文中的 \(S\) 统一指代 \(S^{\prime}\)。显然我们对构造出的新串进行扩展 \(KMP\) 以后,我们只需要关注 \(S\) 的后半段即可。对于构造出的新串 \(S\),\(\forall n + 1 \leq i \leq 2 \times n\),如果以 \(i\) 为左端点的后缀是原串的完美子串,那么必定有 \(nxt_i = m - i\)。由此我们可以在新串中判断完美子串。
考虑维护每个完美子串的出现次数。在 \(S\) 的后半段中,对于一个对应后缀以 \(i\) 为左端点的 完美子串 \(a\),如果存在另一个左端点为 \(j\) 的 子串 \(b\) 使得 \(nxt_j \geq nxt_i\),那么 \(j\) 一定小于 \(i\) 并且 \(a\) 一定是 \(b\) 的子串。因为我们判断时取到了等于,因此完美子串 \(a\) 可能等于子串 \(b\),所以可以统计到完美子串 \(a\) 本身。我们可以考虑维护满足 \(\forall n + 1 \leq j \leq 2 \times n, nxt_j \geq nxt_i\) 的 \(j\) 的个数来统计出子串 \(a\) 出现的次数。具体的实现我们可以使用 后缀和 来维护一个 桶,从而快速求出大于等于某值的元素个数。
总时间复杂度 \(O(n)\),瓶颈在于扩展 \(KMP\),详见代码。
参考代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
int n, m;
int nxt[maxm], cnt[maxn];
char a[maxn], s[maxm];
void get_nxt()
{
int p = 1, q, j = 0;
nxt[0] = m;
while (j + 1 < m && s[j + 1] == s[j])
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 && s[i + j] == s[j])
j++;
nxt[i] = j, p = i;
}
}
}
int main()
{
int ans = 0;
scanf("%s", a);
n = strlen(a);
m = 2 * n + 1;
// 构造新串
for (int i = 0; i < n; i++)
s[i] = a[i];
s[n] = '|';
for (int i = 0; i < n; i++)
s[i + n + 1] = a[i];
// 扩展 KMP
get_nxt();
for (int i = n + 1; i < m; i++)
cnt[nxt[i]]++; // 维护桶
for (int i = n; i >= 1; i--)
cnt[i] += cnt[i + 1]; // 求后缀和
for (int i = n + 1; i < m; i++)
if (nxt[i] == m - i) // LCP长度等于后缀长度即为完美子串
ans++;
printf("%d\n", ans);
// i - n 表示当前枚举的后缀的左端点
// m - i 表示当前枚举的后缀的长度
// 因为 nxt[i] == m - i 时该后缀为完美子串
// 长度为 m - i 的完美子串一定在原串中对应着前缀 [1, m - i]
// 即对应前缀的尾下标为 m - i
// 因此当 i 从大往小枚举时 m - i 递增,符合要求
for (int i = m - 1; i >= n + 1; i--)
{
if (nxt[i] == m - i)
printf("%d %d\n", m - i, cnt[nxt[i]]);
}
return 0;
}