Loading

【题解】CF432D - Prefixes and Suffixes

题目大意

题目链接

给定一个长度为 \(n\) 的模式串 \(S\)。定义 完美子串 表示 \(S\) 的子串中既是 \(S\) 的前缀又是 \(S\) 的后缀的子串。对于给出的模式串 \(S\),试求所有可能的完美子串以及它们出现的位置。输出每个完美子串对应前缀的尾下标以及它在 \(S\) 中出现的次数,按尾下标升序输出。

\(1 \leq n \leq 10^5\)

解题思路

正常来说不会想到用扩展 \(KMP\),但是因为扩展 \(KMP\) 做法代码量比 \(KMP\) 做法略小,因此本文着重讲解扩展 \(KMP\) 解法并赋上代码。扩展 \(KMP\) 做法的字符串下标从 \(0\) 开始。另外的做法还有:

  1. 正解:\(KMP + dp\)
  2. 正解:\(SAM\)\(+ AC\) 自动机)
  3. 乱搞:\(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;
}
posted @ 2021-08-17 22:10  kymru  阅读(94)  评论(0编辑  收藏  举报