牛客竞赛字符串专题 NC237662 葫芦的考验之定位子串(SAM + 后缀链接树上倍增)

文章介绍了如何使用SuffixAutomaton(SAM)数据结构和树上倍增算法来高效地处理字符串子串在原字符串中出现次数的查询。在SAM的基础上,通过预处理和树上倍增,可以在O(logN)时间内回答每个查询,避免了暴力方法导致的时间超时问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述

题意:

给出一个字符串S,|S| ≤ 250000,给出 Q < 250000 次询问,每次需要回答 S[l, r] 在 S 中共出现了多少次。

思路:

如果使用 SAM,我们提前求出每个状态的 cnt[u],询问就是要求我们快速定位 S[l, r] 所在的状态。

我们知道 S[l, r] 一定是 S 的前缀 S[1, r] 的后缀,而 S 的前缀共有 n 个:我们容易找到 S 的每个前缀对应的 SAM 状态节点,不妨设 S[1, i] 对应于状态 ed[i]。

由于 S[l, r] 是 S[l, r] 的后缀,他对应的状态一定位于 ed[i] 的后缀链接上,也即我们要从 ed[i] 到根 root 这条树链上最浅(也就是离根最近,子串结束位置最多,囊括了 S[l, r] 所有结束位置,等价于出现次数)的满足 len[u] >= r - l + 1 的状态。

显然暴力是会超时的,使用树上倍增即可,这里我们使用 dfs 预处理树上倍增要用的 pa 数组。

代码:

ask 函数中 if 里的判断我一开始还联系了节点代表子串长度的最小值 mnl,我写的是:
if(mxl >= leng && mnl <= leng),

这样是不行的,举个例子,比如下面的情况:

在这里插入图片描述
如果像我那样写,图中的第一个链就跳不了了,答案就会出错。

这就属于对倍增的理解不够透彻了,倍增的含义是:从大到小能跳就跳。

因此只需要考虑节点代表子串长度的最大值 mxl 和目标子串长度 leng 即可。

#include<bits/stdc++.h>

using namespace std;

const int N = 2.5e5 + 10, M = N << 1, mx = 20;
int ch[M][26], fa[M], len[M], ed[M], np = 1, tot = 1;
long long cnt[M];
int pa[M][mx];
vector<int> g[M];
char s[N];
int q;

void extend(int c)
{
	int p = np; np = ++tot;
	len[np] = len[p] + 1, cnt[np] = 1, ed[len[np] - 1] = np;
	while (p && !ch[p][c]) {
		ch[p][c] = np;
		p = fa[p];
	}
	if (!p) {
		fa[np] = 1;
	}
	else {
		int q = ch[p][c];
		if (len[q] == len[p] + 1) {
			fa[np] = q;
		}
		else {
			int nq = ++tot;
			len[nq] = len[p] + 1;
			fa[nq] = fa[q], fa[q] = fa[np] = nq;
			while (p && ch[p][c] == q) {
				ch[p][c] = nq;
				p = fa[p];
			}
			memcpy(ch[nq], ch[q], sizeof ch[q]);
		}
	}
}

void dfs(int u, int f)
{
	pa[u][0] = f;
	for (int i = 1; i <= mx - 1; ++i) {
		pa[u][i] = pa[pa[u][i - 1]][i - 1];
	}
	for (auto son : g[u]) {
		dfs(son, u);
		cnt[u] += cnt[son]; //预处理pa数组的同时对后缀链接树进行dp
	}
}

long long ask(int l, int r)
{
	int leng = r - l + 1;
	int p = ed[r];
	for (int i = mx - 1; i >= 0; --i) {
		int ff = pa[p][i];
		int mxl = len[ff];
		if (mxl >= leng) {//当即将倍增跳的父节点代表子串长度最大值大于等于目标串的长度,则跳
			p = ff;
		}
	}
	return cnt[p];
}

signed main()
{
	scanf("%s%d", s, &q);
	for (int i = 0; s[i]; ++i) {
		extend(s[i] - 'a');
	}
	for (int i = 2; i <= tot; ++i) {
		g[fa[i]].emplace_back(i);
	}
	dfs(1, 0);
	while (q--)
	{
		int l, r; scanf("%d%d", &l, &r);
		--l, --r;
		printf("%lld\n", ask(l, r));
	}

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值