【做题记录】后缀数组应用

本文中用 \(he\) 代替 \(height\)\(he_i=\operatorname{lcp}(sa[i],sa[i-1])\)。若无特殊说明,时间复杂度指除去求后缀数组的时间复杂度。

【模板】后缀排序

不同子串个数

\(sa[i]\)\(n-i+1\) 个前缀,与 \(sa[i-1]\) 重复了 \(he_i\) 个,对答案的贡献就是 \(n-i+1-he_i\)

\(\sum\limits_{i=1}^n n-i+1-he_i=n^2+n-\dfrac{n(n+1)}{2}-\sum he_i=\dfrac{n(n+1)}{2}-\sum he_i\)

[USACO5.1] 乐曲主题Musical Themes

题意:求出现至少两次(不可重叠)的子串的最长长度。

转调其实就是让我们求一个差分数组。

考虑二分答案 \(mid\),如果 \(he\) 中存在都不小于 \(mid\) 的连续段 \([L,R]\),此段中原串中最左边的位置(原串中,下同)与最右边的位置之差 \(>=mid\)\(\max\{sa[L-1..R]\}-\min\{sa[L-1..R]\}>=mid\)(此题中应为 \(>mid\),因为若在差分数组中刚好相邻,原串中就会有重叠),说明\(mid\)合法。时间复杂度 \(\operatorname{O}(n\log n)\)

[AHOI2013] 差异

先不管 lcp。长度为 \(l\) 的后缀出现在 \(T_i\) 的次数是 \(l-1\),出现在 \(T_j\) 的次数是 \(n-l\),那么总和是 \(\sum\limits_{l=1}^nl(l-1+n-l)=(n-1)\sum\limits_{l=1}^nl=\frac{1}{2}n(n-1)(n+1)\)

接下来要求两两后缀的 lcp。我们知道 \(\operatorname{lcp}(i,j)=\min\limits_{rnk_i<k\le rnk_j}\{he_k\}\),但即使用 st 表也是 \(\operatorname{O}(n^2)\) 的。考虑每个 \(he_i\) 的贡献。用 单调栈 求出 \(L_i,R_i\),分别是 \(i\) 往前数、往后数第一个 \(he\) 值小于 \(he_i\)的,此时 \(\min\limits_{L_i<k<R_i}\{he_k\}=he_i\),那么 \(he_i\) 对答案的贡献就是 \(he_i(R_i-i)(i-L_i)\)。在原串中相当于 \(sa[L_i..i-1]\)\(sa[i..R_i-1]\) 两两配对,\(\operatorname{lcp}\) 都是 \(he_i\)。这一部分时间复杂度 \(\operatorname{O}(n)\)

[USACO06DEC] Milk Patterns G

题意:求字符串中出现至少 \(k\) 次(可重叠)的子串的最长长度。

假设 \(he\) 中有区间 \([L,R]\)(代表原串中 \(sa[L-1..R]\)),要求出现至少 \(k\) 次,需满足 \(R-(L-1)+1\ge k\)\(R-L+1\ge k-1\)。如果区间 \([L,R]\) 向左或向右扩展一点,\(\operatorname{lcp}\) 一定不会增大。所以只需要对所有长度为 \(k-1\) 的区间的 \(he\) 最小值求最大值即可,用单调队列 \(\operatorname{O}(n)\),st 表\(\operatorname{O}(n\log n)\)

Long Long Message

题意:求两字符串的最长公共子串。

用 SA 解决多串问题时,可以考虑把字符串首尾相连。用特殊字符隔开(每个特殊字符互不相同)可以避免 lcp 横跨两个字符串的问题。

假设连接后字符串为 \(S[1..n]\),分隔字符下标为 \(sep\),题目转化成求 \(\max\limits_{i<sep<j}{\operatorname{lcp}(i,j)}\)。根据排名离得越远,\(\operatorname{lcp}\) 不会更大,可知排名相邻(且来自不同字符串)的两个后缀的 \(\operatorname{lcp}\) 才有可能是答案。

[SDOI2008]Sandy 的卡片

题意:求多个字符串的最长公共子串(此题需先差分)。

沿用上题的套路,把所有字符串拼接在一起(字符串间需用不同字符隔开),并记录第 \(i\) 个字符对应第几个字符串,记为 \(id_i\)

然后二分答案,判定的标准是 \(he\) 中存在一段连续的、大于 \(mid\) 且每个字符串都在 \(id\) 中出现过(这个用桶来做)。

[SDOI2016]生成魔咒

题意:初始时有一空串,每次在末尾加入一个字符,并求出当前串的不同子串数。

SA 中插入不太好搞,反过来想,把字符串翻转,每次从开头删去一个字符。

如果一个字符被删去,我们称以这个字符开头的后缀被删去。记 \(pre_i\) 为排名在 \(i\) 之前且最大的后缀排名,\(nxt_i\) 同理(模拟一个链表)。求出 \(he\) 后,我们把 \(he_i\) 的定义改为 \(\operatorname{lcp}(pre_i,i)\)

前面提到过长度为 \(n\) 的字符串的不同子串数为 \(\dfrac{n(n+1)}{2}-\sum he_i\)。维护一下当前的 \(\sum he_i\),删去排名为 \(i\) 的后缀就等同于在链表中删去第 \(i\) 个节点。同时维护一下 \(he_{nxt_i}\)(显然只会对这个产生影响)。

【模板】后缀自动机(SAM)

题意:求子串长度 \(\times\) 出现次数的最大值。

首先不难想到这个子串的长度一定是 \(he_i\) 之一。

那么就可以用单调栈求出排名为 \(i\) 的子串出现了多少次。


以下题目都有一定难度,请谨慎食用

P3975 [TJOI2015]弦论

题意:求第 \(k\) 小子串,分相同子串算一个 / 多个 两种情况。

相同子串算一个的很好处理,这里就不说了。

相同子串算多个的情况用 SA 做确实有点复杂 不过既然写在这里肯定是可以哒

排名为 \(i\) 的后缀,有 \(n-sa_i+1\) 个前缀

再对这东西做一个前缀和,记为 \(sum_i\)

然后建一个 st 表。

考虑对 \(he\) 分组,要求每组只有第一个 \(he\)\(0\)

\(he=0\) 代表这两个后缀第一个字母都不相同,显然它们不会有任何前缀是相同的。那么就可以判断答案在哪个组里。

设这个组的左右端点(后缀排名)分别是 \(L, R\),要求的排名是 \(k\)

先找到区间内 \(\min\{\operatorname{lcp}\}\) 的位置,记为 \(mid\)

容易发现排名在 \([L,R]\) 中的后缀,它们长度为 \(he_{mid}\) 的前缀都相同

于是可以判断答案是否在这些子串里,即判断 \(k\) 是否 \(\leq (R-L+1)\times he_{mid}\)。如果是,进而可以推出长度是多少。

如果不是,判断答案在 \(mid\) 左还是右(除去这些长度为 \(he_{mid}\))的子串

然后更新一下 \(k,L,R\) 就好了。

注意要记录上一次的 \(he_{mid}\),然后处理一些细节。

每次区间长度至少减少 \(1\),这一部分时间复杂度 \(O(n)\)

inline int get_min(int x, int y) {
	return he[x] < he[y] ? x : y;
}

/*
SA
*/

void solve0() {
	rep(i, 1, n) {
		if(k > n - sa[i] + 1 - he[i]) k -= n - sa[i] + 1 - he[i];
		else {
			print(sa[i], sa[i] + he[i] + k - 1);
			return ;
		}
	}
	puts("-1");
}

void build_st() {
	rep(i, 1, n) sum[i] = sum[i - 1] + n - sa[i] + 1;
	rep(i, 2, n) lg[i] = lg[i / 2] + 1;
	rep(i, 1, n) st[i][0] = i;
	rep(len, 1, lg[n]) rep(i, 1, n - (1 << len) + 1)
		st[i][len] = get_min(st[i][len - 1], st[i + (1 << len - 1)][len - 1]);
}

inline ll get_sum(int l, int r) {
	return sum[r] - sum[l - 1];
}

inline int query(int l, int r) {
	l ++ ;
	int k = lg[r - l + 1];
	return get_min(st[l][k], st[r - (1 << k) + 1][k]);
}

void solve1() {
	int l = 1, r, mid, now = 0;
	ll tmp;
	for(; l <= n; ++ l)
		if(he[l] == 0) {
			for(r = l; he[r + 1] > 0; ++ r) ;
			if(k <= get_sum(l, r)) break;
			k -= get_sum(l, r);
			l = r;
		}
	while(l < r) {
		mid = query(l, r);
		tmp = 1ll * (r - l + 1) * (he[mid] - now); // 注意计算个数时要减去上一次的长度
		if(he[mid] > now && k <= tmp) {
			print(sa[l], sa[l] + now + (k - 1) / (he[mid] - now));
			return ;
		}
		k -= tmp;
		tmp = get_sum(l, mid - 1) - 1ll * he[mid] * (mid - l);
		if(k <= tmp) r = mid - 1;
		else k -= tmp, l = mid;
		now = he[mid];
	}
	print(sa[l], sa[l] + now + k - 1);
}

signed main() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	scanf("%d%d", &T, &k);
	SA();
	if(!T) solve0();
	else if(1ll * n * (n + 1) / 2 < k) puts("-1");
	else {
		build_st();
		solve1();
	}
	return 0;
}

[HEOI2016/TJOI2016]字符串

考虑二分答案。设当前答案为 \(len\)

那么要判断是否存在一个后缀 \(p\),使得 \(a\le p\le b-len+1\)\(\operatorname{lcp}(p,c)\ge len\)

考虑一个后缀的排名 \(k\)

\(k<rnk_c\) 时,如果 \(k\) 越小,\(\min\limits_{k<i\le rnk_c}\{he_i\}\) 单调不增。

\(k>rnk_c\) 时同理。

那么,使得 \(\operatorname{lcp}(p,c)\ge len\) 的后缀 \(p\)排名 一定是一段区间。可以用 st 表加二分求出这个排名区间。

把下标看成第一维,排名看成第二维,变成了一个二维数点问题。再用一个主席树即可。

时间复杂度 \(O(n\log^2n)\)

int build(int l, int r) {
	int p = ++ tot;
	if(l == r) return p;
	int mid = l + r >> 1;
	lc[p] = build(l, mid);
	rc[p] = build(mid + 1, r);
	return p;
}

int modify(int rt, int l, int r, int t) {
	int p = ++ tot;
	sum[p] = sum[rt] + 1;
	if(l == r) return p;
	int mid = l + r >> 1;
	if(t <= mid) {
		lc[p] = modify(lc[rt], l, mid, t);
		rc[p] = rc[rt];
	} else {
		rc[p] = modify(rc[rt], mid + 1, r, t);
		lc[p] = lc[rt];
	}
	return p;
}

int query(int p, int l, int r, ci &tl, ci &tr) {
	if(tl <= l && r <= tr) return sum[p];
	int mid = l + r >> 1, res = 0;
	if(tl <= mid) res += query(lc[p], l, mid, tl, tr);
	if(mid < tr) res += query(rc[p], mid + 1, r, tl, tr);
	return res;
}

inline int query(int x, int y, int l, int r) {
	return query(rt[y], 1, n, l, r) - query(rt[max(x - 1, 0)], 1, n, l, r);
}

void solve() {
	int a, b, c, d, rnkL, rnkR, lenl, lenr, len, ans = 0;
	scanf("%d%d%d%d", &a, &b, &c, &d);
	lenl = 0; lenr = min(d - c + 1, b - a + 1);
	while(lenl <= lenr) {
		len = lenl + lenr >> 1;
		rnkL = rnkR = rnk[c];
		per(i, lg[rnk[c] - 1], 0)
			if(lcp(rnkL - (1 << i), rnk[c]) >= len)
				rnkL -= 1 << i;
		per(i, lg[n - rnk[c] + 1], 0)
			if(lcp(rnk[c], rnkR + (1 << i)) >= len)
				rnkR += 1 << i;
		if(query(a, b - len + 1, rnkL, rnkR)) ans = len, lenl = len + 1;
		else lenr = len - 1;
	}
	printf("%d\n", ans);
}

signed main() {
	scanf("%d%d", &n, &q);
	scanf("%s", s + 1);
	SA();
	get_height();
	build_st();
	rt[0] = build(1, n);
	rep(i, 1, n) rt[i] = modify(rt[i - 1], 1, n, rnk[i]);
	for(; q; -- q) solve();
	return 0;
}
posted @ 2021-05-25 16:42  EverlastingEternity  阅读(56)  评论(0编辑  收藏  举报