洛谷 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} 1≤n≤5×105,0≤t≤1,1≤k≤109
题解报告
后缀自动机中,每一个节点代表一种状态,代表 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;
}
收获&总结
感觉后缀自动机好抽象…还是得多刷刷相关题目,个人对后缀自动机的理解太浅了。(其实是太迷了,总结不出来啥)