zl程序教程

您现在的位置是:首页 >  其它

当前栏目

【ybt金牌导航2-3-3】【luogu P3975】K小子串 / 弦论

导航 Luogu ybt 金牌
2023-09-27 14:28:28 时间

K小子串 / 弦论

题目链接:ybt金牌导航2-3-3 / luogu P3975

题目大意

给你一个字符串,要你求字典序第 k 小的子串。
(相同的子串可能算一个,也可能算多个,数据以读入 0/1 来判断)

思路

考虑用 SAM 来做。

如果相同的子串算一个,那其实就是这一道题,直接看这道题的题解就可以了。
——>链接<——

接着我们来看如果相同的子串算多个怎么做。
那很明显就是要求出这个子串有多少个。
那我们可以通过 DP 来求。考虑到 f a i fa_i fai 的性质(后缀),我们可以用它的逆拓扑序顺序枚举点 i i i,然后 f a i fa_i fai 的个数加上它的个数,设求出的是 s i z e i size_i sizei
(其实相同的子串算一个的话就其实就把 s i z e i size_i sizei 都等于 1 1 1

然后由于你有一些点是复制得到的,那那些点的 s i z e size size 初始化的时候就是 0 0 0,不是复制的初始化就是 1 1 1,不然你会一个加进去的字符可能会因此计算两次。

接着就跟之前一样 DP,求出 i i i 出发每个子串的这个值的和。

然后就跟相同算一个时的做法一样了,直接用来比较的值变了。
而且你输出这个字符去找下一个的之后,原来我们只用减一,是因为相同的只算一个,但现在算多个了,那减去的就是 s i z e i size_i sizei

然后就好了。

代码

#include<cstdio>
#include<cstring>
#define ll long long

using namespace std;

struct node {
	int len, fa, size;
	ll f, size_s;
	int son[31];
	node() {
		memset(son, 0, sizeof(son));
		len = fa = size = 0;
		f = -1ll;
		size_s = 0ll;
	}
}d[1000001];
int n, tot, lst, T, k;
int tong[1000001], xx[1000001];
char s[500001];

void SAM_build(int now) {
	int p = lst;
	int np = ++tot;
	d[np].len = d[p].len + 1;
	lst = np;
	d[np].size = 1;//新增点的大小是1
	
	for (; p && !d[p].son[now]; p = d[p].fa)
		d[p].son[now] = np;
	
	if (!p) d[np].fa = 1;
		else {
			int q = d[p].son[now];
			if (d[q].len == d[p].len + 1) d[np].fa = q;
				else {
					int nq = ++tot;
					d[nq] = d[q];
					d[nq].size = 0;//复制点的大小是0
					d[nq].len = d[p].len + 1;
					d[q].fa = nq;
					d[np].fa = nq;
					for (; p && d[p].son[now] == q; p = d[p].fa) {
						d[p].son[now] = nq;
					}
				}
		}
}

void dfs(int now) {
	if (d[now].f != -1) return ;
	d[now].f = 0;
	for (int i = 0; i < 26; i++)
		if (d[now].son[i]) {
			dfs(d[now].son[i]);
			d[now].f += d[d[now].son[i]].f;
		}
	d[now].f++;
}

void work(int now, int num) {
	while (num) {
		for (int i = 0; i < 26; i++)
			if (d[now].son[i]) {
				if (d[d[now].son[i]].f >= num) {
					putchar(i + 'a');
					now = d[now].son[i];
					num--;
					break;
				}
				else num -= d[d[now].son[i]].f;
			}
	}
}

void get_size() {
	for (int i = 0; i <= n; i++)//奇数排序按len从大到小排
		tong[i] = 0;
	for (int i = 1; i <= tot; i++)
		tong[d[i].len]++;
	for (int i = 1; i <= n; i++)
		tong[i] += tong[i - 1];
	for (int i = 1; i <= tot; i++)
		xx[tong[d[i].len]--] = i;
	
	for (int i = tot; i >= 1; i--) {
		d[d[xx[i]].fa].size += d[xx[i]].size;//转移得到 1 到 xx[i] 形成的子串的出现次数
	}
	
	d[1].size = 0;//DP 求出从前缀是 1 到 xx[i] 形成的串的有多少个子串
	for (int i = tot; i >= 1; i--) {
		d[xx[i]].size_s = d[xx[i]].size;
		for (int j = 0; j < 26; j++)
			if (d[xx[i]].son[j])
				d[xx[i]].size_s += d[d[xx[i]].son[j]].size_s;
	}
}

void work_s(int now, int num) {
	num -= d[1].size;
	while (num > 0) {
		for (int i = 0; i < 26; i++)
			if (d[now].son[i]) {
				if (d[d[now].son[i]].size_s >= num) {
					putchar(i + 'a');
					now = d[now].son[i];
					num -= d[now].size;//记得这里是减去它这个子串,之前只算一个只用减一,现在要减的就是这个子串的个数了
					break;
				}
				else num -= d[d[now].son[i]].size_s;
			}
	}
}

int main() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	scanf("%d %d", &T, &k);
	
	tot = lst = 1;
	for (int i = 1; i <= n; i++)
		SAM_build(s[i] - 'a');
	
	dfs(1);
	
	if (!T) {
		if (k > d[1].f) {
			printf("-1");
			return 0;
		}
		work(1, k);
	}
	else {
		get_size();
		if (k > d[1].size_s) {
			printf("-1");
			return 0;
		}
		work_s(1, k);
	}
	
	return 0;
}