洛谷 [P3975 [TJOI2015]弦论

洛谷 P3975 [TJOI2015]弦论

题目描述

给定一个长度为 n n n 的字符串,求它的第 k k k 小字串;给定 t t t t t t 0 0 0 则表示不同位置的相同子串算作一个, t t t 1 1 1 则表示不同位置的相同子串算作多个。

数据范围

1 ≤ n ≤ 5 × 1 0 5 , 0 ≤ t ≤ 1 , 1 ≤ k ≤ 1 0 9 1≤ n ≤5×10^5,0\leq t \leq 1,1\leq k \leq 10^{9} 1n5×1050t11k109


题解报告

后缀自动机中,每一个节点代表一种状态,代表 e n d p o s endpos endpos 相同的一些字符串的集合。

任意从初始状态 t 0 t_0 t0 开始的路径,如果我们将转移路径上的标号写下来,都会形成 s s s 的一个子串。反之每个 s s s 的子串对应从 t 0 t_0 t0 开始的某条路径。

t = 1 t = 1 t=1 时,每个字串的个数即为该字串对应的状态 v v v e n d p o s endpos endpos 集合大小。

t = 0 t = 0 t=0 时,相当于每个状态 v v v e n d p o s endpos endpos 大小变为 1 1 1

e n d p o s endpos endpos 集合大小如何求?

后缀自动机有个性质,所有节点和 l i n k link link 转移边及根节点构成一棵树。

每个状态的 e n d p o s endpos endpos 集合为其子节点 e n d p o s endpos endpos 集合的及自身的并(可以直接加,因为子节点之间的集合一定不相交)

往自动机中不断插入新字符的过程中,

令加入新字符后整个字符串对应的状态 c u r cur cur e n d p o s endpos endpos 集合大小为 1 1 1

在后缀链接形成的树上 d f s dfs dfs 一遍或者拓扑排序就可以求得所有节点的 e n d p o s endpos endpos 集合大小。

然后要求出每个点包含多少字串。

这个可以用 d f s dfs dfs (容易报栈)求,

或者利用计数排序,按长度排序,从大到小更新每个节点包含多少字串。

求第 k k k 大,在以转移边形成的字典树上操作。

转移边从 ′ a ′ 'a' a 枚举到 ′ z ′ 'z' z,当前字符包含字串数量小于 k k k 的话就减去,

大于的话就输出当前字符,然后进入该节点。

AC代码:

#include <bits/stdc++.h>
#define ll long long
#define int ll
using namespace std;
const int maxn = 1e6 + 5;

int n, t, k, last, tot;
int link[maxn], len[maxn];
int in[maxn], cnt[maxn], sum[maxn];
int sa[maxn], bowl[maxn];
int nxt[maxn][30];
bool vis[maxn];
char s[maxn];


void init () {link[0] = -1;}

void sam_ins (int num) {
	int cur = ++tot;
	len[cur] = len[last] + 1;
	int p = last;
	while (p != -1 && !nxt[p][num]) {
		nxt[p][num] = cur;
		p = link[p];
	}
	if (p == -1) link[cur] = 0;
	else {
		int q = nxt[p][num];
		if (len[p] + 1 == len[q]) link[cur] = q, ++in[q];
		else {
			int clone = ++tot;
			len[clone] = len[p] + 1;
			link[clone] = link[q];
			memcpy (nxt[clone], nxt[q], sizeof nxt[q]);
			while (p != -1 && nxt[p][num] == q) {
				nxt[p][num] = clone;
				p = link[p];
			}
			link[cur] = link[q] = clone, in[clone] += 2;
		}
	}
	last = cur;
	++cnt[cur];
}

void topu () {
	queue <int> q;
	for (int i = 1; i <= tot; ++i) if (!in[i]) {
		q.push (i);
	}
	int now;
	while (!q.empty ()) {
		now = q.front ();
		q.pop ();
		cnt[link[now]] += cnt[now];
		--in[link[now]];
		if (!in[link[now]]) q.push (link[now]);
	}
}

/*void dfs (int now) {
	sum[now] = cnt[now];
	for (int i = 0; i < 26; ++i) {
		if (nxt[now][i]) {
			int v = nxt[now][i];
			if (!vis[v]) dfs (v);
			vis[v] = true;
			sum[now] += sum[v];
		}
	}
}*/

string get_ans () {
	string ans;
	int now = 0;
	bool ok = true;
	while (ok) {
		ok = false;
		k -= cnt[now];
		if (k <= 0) break;
		for (int i = 0; i < 26; ++i) {
			if (nxt[now][i]) {
				if (k > sum[nxt[now][i]])
				k -= sum[nxt[now][i]];
				else {
					ans += 'a' + i;;
					now = nxt[now][i];
					ok = true;
					break;
				}
			}
		}
	}
	return ans;
}

void charming () {
	init ();
	cin >> s + 1;
	n = strlen (s + 1);
	for (int i = 1; i <= n; ++i) sam_ins (s[i] - 'a');
	cin >> t >> k;
	if (t) topu ();
	else fill (cnt + 1, cnt + 1 + tot, 1);
	for (int i = 1; i <= tot; ++i) ++bowl[len[i]];
	for (int i = 1; i <= tot; ++i) bowl[i] += bowl[i - 1];
	for (int i = tot; i >= 0; --i) sa[bowl[len[i]]--] = i;
	for (int i = 1; i <= tot; ++i) sum[i] = cnt[i];
	sum[0] = cnt[0] = 0;
	for (int i = tot; i >= 0; --i) {
		for (int j = 0; j < 26; ++j) if (nxt[sa[i]][j]) {
			sum[sa[i]] += sum[nxt[sa[i]][j]];
		}
	}
	if (sum[0] < k) cout << -1 << endl;
	else cout << get_ans () << endl;
}

signed main () {
	charming ();
	return 0;
}

收获&总结

感觉后缀自动机好抽象…还是得多刷刷相关题目,个人对后缀自动机的理解太浅了。(其实是太迷了,总结不出来啥)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值