SUBLEX - Lexicographical Substring Search
题目链接:luogu SP7258
题目大意
给你一个字符串,要你求字典序第 k 大的子串。
(相同的子串算只一个)
思路
我们考虑用 SAM 来做。
考虑之前我们求出了 f i f_i fi,就是后缀自动机上从 i i i 出发的不同子串个数(包括空串)。
那我们考虑用这样的方式:(有点类似线段树上找第
k
k
k 个的位置)
从起点
1
1
1 开始,现在是
x
x
x,按字典序从小到大开始枚举要新加的字符,然后就在后缀自动机上走。
如果有这一条边
z
z
z 字符,连向
y
y
y,那你就看
f
y
f_y
fy 与
k
k
k 的关系。
如果
f
y
≤
k
f_y\leq k
fy≤k,那就说明第
k
k
k 个的这一位是这个字符,就把这个字符输出,然后现在起点变成
y
y
y,
k
−
=
1
k-=1
k−=1(因为要减去
y
y
y 这个代表的最长后缀,它有
z
z
z 这个字符,那我们现在去处理下一个,那它也过掉了)。
那如果
f
y
>
k
f_y>k
fy>k,那就将
k
−
=
f
y
k-=f_y
k−=fy
当 k = 0 k=0 k=0 的时候,你就可以直接退出了,此时 1 1 1 从后缀自动机上跳到你现在的点的路径所代表的子串就是你要的子串。
代码
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
struct node {
int len, fa, size;
ll f;
int son[31];
node() {
memset(son, 0, sizeof(son));
len = fa = 0;
f = -1ll;
}
}d[200001];
int n, tot, lst, T, k;
char s[100001];
void SAM_build(int now) {
int p = lst;
int np = ++tot;
d[np].len = d[p].len + 1;
lst = np;
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].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;
}
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
tot = lst = 1;
for (int i = 1; i <= n; i++)
SAM_build(s[i] - 'a');
dfs(1);
scanf("%d", &T);
while (T--) {
scanf("%d", &k);
work(1, k);
printf("\n");
}
return 0;
}