【后缀自动机】Luogu P3975 [TJOI2015]弦论题解

[TJOI2015]弦论

题目描述

为了提高智商,ZJY 开始学习弦论。这一天,她在《String theory》中看到了这样一道问题:对于一个给定的长度为 n n n 的字符串,求出它的第 k k k 小子串是什么。你能帮帮她吗?

输入格式

第一行是一个仅由小写英文字母构成的字符串 s s s

第二行为两个整数 t t t k k k t t t 0 0 0 则表示不同位置的相同子串算作一个, t t t 1 1 1 则表示不同位置的相同子串算作多个。 k k k 的意义见题目描述。

输出格式

输出数据仅有一行,该行有一个字符串,为第 k k k 小的子串。若子串数目不足 k k k 个,则输出 − 1 -1 1

样例 #1

样例输入 #1

aabc
0 3

样例输出 #1

aab

样例 #2

样例输入 #2

aabc
1 3

样例输出 #2

aa

样例 #3

样例输入 #3

aabc
1 11

样例输出 #3

-1

提示

数据范围

对于 10 % 10\% 10% 的数据, n ≤ 1000 n\leq 1000 n1000

对于 50 % 50\% 50% 的数据, t = 0 t = 0 t=0

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 5 × 1 0 5 1\leq n \leq 5 \times 10^5 1n5×105 0 ≤ t ≤ 1 0\leq t \leq 1 0t1 1 ≤ k ≤ 1 0 9 1\leq k \leq 10^9 1k109


SOLUTION

子串计数可以用后缀自动机。

后缀自动机的大致思路:

每一个节点保存 e n d p o s \mathrm{endpos} endpos (结尾出现位置)集合相同的子串(状态),往 SAM 中添加节点时, l a s t \mathrm{last} last (上次操作的末尾)的每一个后缀状态( e n d p o s \mathrm{endpos} endpos 不同)若没有为添加字符的连边,则向新节点连添加字符的连边显然 )。该点需要通过 l i n k \mathrm{link} link后缀连接)连向该状态中,最大的一个字符串,它的最大的后缀使得二者的 e n d p o s \mathrm{endpos} endpos 不同,如果不存在则为 0 0 0 ,存在且为它所在的状态的最大字符串,则将 l i n k \mathrm{link} link 连向那个状态,否则,创造一个新的节点,克隆那个状态除了 l e n \mathrm{len} len (最大串)的信息,再将那个连向状态的且再 l a s t \mathrm{last} last 的后缀状态中的边重定向到克隆节点。

这样 SAM 就是一个 D A G DAG DAG ,每一条路径就是子串(不重不漏),沿着最后一个加入的字符对应的节点跳 l i n k \mathrm{link} link , 每一个经过的节点都是终止节点,即到该节点的路径为后缀。

本题中,对于 t = 0 t = 0 t=0 的情况( r o o t \mathrm{root} root 为初始状态):

d u d_u du 表示从状态 u u u 出发向后的路径条数,那么 d r o o t d_{root} droot 就是本质不同的子串的个数

那么可以得到 d u d_u du 的转移方程( E \mathrm{E} E 表示边集):

d u = 1 + ∑ ( u , v , c ) ∈ E d v d_u = 1 + \sum\limits_{(u,v,c)\in\mathrm{E}}d_v du=1+(u,v,c)Edv

之后,我们对整个 D A G DAG DAG 再进行一遍 D F S DFS DFS ,对于每个节点,按边 'a'~'z' 的顺序枚举,就可以使得枚举从小到大,则:

记录枚举到的所有状态 v v v d v d_v dv 的和为 s u m sum sum

若按照边 i i i 枚举到的下一个状态 v ′ v' v s u m + v ′ < k sum + v' < k sum+v<k ,就 s u m + = k sum += k sum+=k,此时第 k k k 小一定不从 v ′ v' v 经过。

否则,第 k k k 小一定从 v ′ v' v 经过,那么我们令 s u m + + sum++ sum++ ,即为状态 v ′ v' v 中最小的子串,并进入 v ′ v' v 递归。

等到 s u m = k sum = k sum=k 时,说明已经找到了答案,返回即可。

复杂度 O ( N ) O(N) O(N) ,这样就可以做到 50 p t s 50\mathrm {pts} 50pts

下面考虑如何将 t = 1 t=1 t=1 的情况转化为第一种:

此时要求算上重数,我们可以通过 e n d p o s \mathrm{endpos} endpos 集合的大小来反映子串出现次数。

而对于一个节点 u u u ,考虑 l i n k \mathrm{link} link 指向它的所有节点 v i v_i vi,那么状态 u u u 中的子串均为 v i v_i vi 中的子串的后缀,且根据 l i n k \mathrm{link} link 的定义,SAM建立的过程中必然存在相应的转移,使得 v i v_i vi e n d p o s \mathrm{endpos} endpos 集合的交必然 “几乎” 与 u u u 的相等(这段可以掠过 ),即

S i z e u Size_u Sizeu 表示 u u u e n d p o s \mathrm{endpos} endpos 的大小,

u u u 不是克隆节点,则 S i z e u = 1 + ∑ S i z e v i Size_u=1+\sum Size_{v_i} Sizeu=1+Sizevi, 例如 'p''pop' 中,包含 'p' 的一个子串就是它本身,所以不会有其他状态转移这个信息,因此要加一。

u u u 是克隆节点,则 S i z e u = ∑ S i z e v i Size_u=\sum Size_{v_i} Sizeu=Sizevi,例如 ab'cabdab' 中,所有 'ab' 出现的位置都可以被其他子串包含(因为克隆节点是从其他子串中剥离出来的),因此不用加一。

因此可以用拓朴排序处理出这个东西。

接着,我们知道从 r o o t root root 到任意节点的路径为子串,结束位置的 e n d p o s \mathrm{endpos} endpos 的大小就是它出现的次数,因此从一个节点 u u u 往后的路径(或者选择方式)的数量即为里面经过的状态的 S i z e Size Size 的和,这样我们就可以转化为第一种情况,此时:

d u = S i z e u + ∑ ( u , v , c ) ∈ E d v d_u = Size_u + \sum\limits_{(u,v,c)\in\mathrm{E}}d_v du=Sizeu+(u,v,c)Edv

不过需要注意的是,在进行最后的 D F S DFS DFS 时,如果 s u m + S i z e v i ≥ k sum+Size_{v_i} \ge k sum+Sizevik v i v_i vi 是未访问的下一个状态),那么我们的答案路径肯定是以 v i v_i vi 结尾的(因为算重,所以此时有多种选择),这时候就需要直接返回,否则我们需要跳过当前状态节点进入下一个状态的选择,就得让 s u m + = S i z e v i sum += Size_{v_i} sum+=Sizevi

这样算法的时空复杂度为 O ( N ) O(N) O(N) ,具体实现看代码。


AC CODE

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#define int long long
using namespace std;

const int N = 5e5 + 10;

struct state
{
	int len, link;
	int Next[26];
}st[N << 1];

char s[N];
int n, t, k;
int deg[N << 1], Size[N << 1], d[N << 1];
int sz, last;
bool flag;

void sam_init()
{
	st[0].len = 0; st[0].link = -1;
	sz ++ ; last = 0;
}

void sam_extended(int c)
{
	int cur = sz ++ , p = last; Size[cur] = 1;
	st[cur].len = st[p].len + 1;
	while(p != -1 && st[p].Next[c] == 0)
	{
		st[p].Next[c] = cur;
		p = st[p].link;
	}
	if(p == -1) st[cur].link = 0, deg[0] ++ ;
	else
	{
		int q = st[p].Next[c];
		if(st[q].len == st[p].len + 1) st[cur].link = q, deg[q] ++ ;
		else
		{
			int clone = sz ++ ;
			st[clone].len = st[p].len + 1;
			st[clone].link = st[q].link;
			memcpy(st[clone].Next, st[q].Next, sizeof st[q].Next);
			while(p != -1 && st[p].Next[c] == q)
			{
				st[p].Next[c] = clone;
				p = st[p].link;
			}
			st[q].link = st[cur].link = clone;
			deg[clone] += 2;
		}
	}
	last = cur;
}

void topsort()
{
	queue<int> q;
	for(int i = 0; i <= sz; i ++ )
		if(deg[i] == 0) q.push(i);
	while(!q.empty())
	{
		int x = q.front(); q.pop();
		if(st[x].link != -1)
		{
			Size[st[x].link] += Size[x];
			if((--deg[st[x].link]) == 0) q.push(st[x].link);
		}
	}
}

int dfs(int x, int op)
{
	if(d[x]) return d[x];
	d[x] = (op ? Size[x] : 1);
	for(int i = 0; i < 26; i ++ )
	{
		if(!st[x].Next[i]) continue;
		d[x] += dfs(st[x].Next[i], op);
	}
	return d[x];
}

void solve(int x, int op, int sum)
{
	if(sum == k) return;
	for(int i = 0; i < 26; i ++ )
	{
		if(!st[x].Next[i]) continue;
		if(sum + d[st[x].Next[i]] < k) { sum += d[st[x].Next[i]]; continue; }
		putchar(i + 'a'); flag = 1;
		if(op && sum + Size[st[x].Next[i]] > k) return;
		solve(st[x].Next[i], op, sum + (op ? Size[st[x].Next[i]] : 1));
		return; 
	}
}

signed main()
{
	scanf("%s%lld%lld", s + 1, &t, &k);
	n = strlen(s + 1); sam_init();
	for(int i = 1; i <= n; i ++ ) sam_extended(s[i] - 'a');
	if(t == 1) topsort();
	dfs(0, t); solve(0, t, 0);
	if(!flag) printf("-1");
	putchar('\n');
	return 0;
}

END.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值