感觉啥都不会就来补自己的弱项了……
\(\text{Solution:}\)
第 \(k\) 小的子串,这东西长得很平衡树。
回忆一下,我们在平衡树上找第 \(k\) 大的做法:记录左右孩子的 \(siz,\) 与 \(k\) 比较大小,不断二分。
那么,同样地,在这题里面,考虑如何类似地在 SAM 上面做这件事情。
先考虑一下 \(t=0\) 的情况。这个时候实际上是本质不同的第 \(k\) 小子串。
那么当我们在 SAM 上面走到一个点的时候,无非要考虑的就是下一步走哪里。如果我们可以知道走每一个点会到达的所有子串个数,那不就顺其自然实现了一个类似上述二分的选择过程了吗。
具体地,从字典序小的转移边枚举,如果当前 \(k\) 小于等于走这条边到达的点中所有本质不同子串个数,那么第 \(k\) 小自然也包含在里面,沿着这条边往下走即可;否则,减去这个点的 \(siz,\) 继续匹配。
那么如何处理上述的 经过一个点能到达的子串个数 呢?考虑一个经典 \(dp.\)
设 \(f_i\) 表示经过 \(i\) 点往后走能走到的本质不同子串个数。显然,终止节点的 \(f=1,\) 考虑逆推,那一个点的 \(f\) 显然就是其所有转移边所连接的点的 \(f\) 之和。
注意到 SAM 是一个 DAG ,所以我们可以在上面 \(dp.\) 这里可以 \(O(n)\) 处理出来,然后在上面跑上述算法过程输出即可。
无解的话就是直接判断总的子串个数与 \(k\) 的大小关系。
到此为止 \(t=0\) 的已经解决。考虑 \(t=1\) 如何做:
位置不同,意味着上述一个本质不同子串的贡献就不再是 \(1\) 了,而是其对应的 endpos 集合的大小。
因为一个子串的出现次数就相当于其 endpos 的集合大小。
先考虑如何维护出 endpos 集合大小:观察其 parent 树,有性质:一个点的 endpos 是其所有子节点的 endpos 之并,并且所有子节点的 endpos 没有交集。
所以,对应一个点的 endpos 集合大小,实际上就是其在 parent 树上对应子树的 siz 大小。这里我们可以直接选择建立出 parent 树求解。
继续考虑,这两个问题的本质区别是什么?原先我们走过一个串只需要考虑其是不是本质不同,现在需要考虑的还需要算上它的出现次数。
于是,我们考虑将原来的每个点的 “点权” 从 \(1\) 变成其 endpos 集合的大小。具体地,之前计算的 \(f\) 初值是 \(1\) (因为要算上这个点本身),现在我们将其初值改为 \(siz[i]\) 就可以计算好了。
考虑同样的 查找 过程:如果走到了一个点,当前剩余的 \(k\) 要小于等于它在总串中的出现次数,意味着答案就是它,直接返回即可。
这里可能会有一个疑问:我们知道,SAM 上面一个点维护的并不是一个子串,而是一堆 endpos 集合相同的子串,为什么直接返回不用考虑其他情况?
实际上,对于比这个串大的,endpos 集合相同的串,在之前的查找过程中已经被舍弃了;而更大的显然更不予考虑。
还有一些细节:如何维护 SAM 上面的 \(siz\) ?
考虑我们把一个串中每个 前缀 对应的节点的 \(siz\) 置为 \(1,\) 因为它们的前面无法再添加字符,也易知它们是 parent 树上最深的节点。这个其实在建立 SAM 的时候直接把新建的一个节点的 \(siz\) 置为 \(1\) 即可。
然后把树建立出来直接跑就好了。每一个前缀对应的节点代表了一个初始 endpos 等价类。
代码中是一种赋值方法,不太清楚的话也可以建立完 SAM 之后从 \(1\) 开始依次匹配 \(S\) 的字母获得每一个前缀的位置,将其 \(siz\) 置为 \(1\) 即可。
这里注意, endpos 不是简单的树上求 \(siz\) …… 它们是有一点区别的 ,你会发现,parent 树上的节点,它的 endpos 是由其子节点的 endpos 并起来的。所以你只能把你子节点的 \(siz\) 加起来,而不是和一棵树一样每一个点都是初值为 \(1.\)
这个题讲到这里就差不多结束了。注意的几个细节:
-
SAM 上面一个节点对应的是许多 endpos 集合相同的子串。
-
parent 树上一个节点代表的 endpos 是其子节点的并。
-
维护 parent 树的 \(siz\) 只有叶子节点的 \(siz\) 初始值为 \(1.\)
-
不要不小心把 DAG 上面的 \(dp\) 和 parent 树上的 \(dp\) 搞混,要在树上搞事情就别忘了建树或者用基数排序的 trick.
-
仔细想好 SAM 上面节点维护的信息,例如其转移边的意义,根据题目你需要维护什么样不同的信息……思维清晰才能做题
-
SAM 是一个 DAG ,在上面 \(dp\) 别忘了 \(vis\) 数组做记忆化。
-
建立 SAM 的时候不要忘了把
tot,last
设置为 \(1\) !!!!
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
namespace SAM{
int len[N],pa[N],ch[N][26],tot,last,siz[N],f[N],vis[N];
char ans[N];
vector<int>G[N];
void Ins(const int &c){
int p=last;
int np=++tot;
last=tot;
len[np]=len[p]+1;f[np]=1;
for(;p&&!ch[p][c];p=pa[p])ch[p][c]=np;
if(!p)pa[np]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])pa[np]=q;
else{
int nq=++tot;
len[nq]=len[p]+1;
pa[nq]=pa[q];pa[np]=pa[q]=nq;
memcpy(ch[nq],ch[q],sizeof ch[q]);
for(;p&&ch[p][c]==q;p=pa[p])ch[p][c]=nq;
}
}
}
void dfs(int root){
for(int i=0;i<(int)G[root].size();++i){
int v=G[root][i];
dfs(v);
f[root]+=f[v];
}
}
void dfs2(int x){
if(vis[x])return;
vis[x]=1;
for(int i=0;i<26;++i){
if(!ch[x][i])continue;
dfs2(ch[x][i]);
siz[x]+=siz[ch[x][i]];
}
}
void query(int x,int k){
if(k<=f[x])return;
k-=f[x];
for(int i=0;i<26;++i){
int v=ch[x][i];
if(!v)continue;
if(k>siz[v])k-=siz[v];
else{
putchar(i+'a');
query(v,k);
return;
}
}
}
}
using namespace SAM;
char s[N];
int t,k,n;
int main(){
scanf("%s",s+1);
scanf("%d%d",&t,&k);
n=strlen(s+1);last=tot=1;
for(int i=1;i<=n;++i)Ins(s[i]-'a');
for(int i=2;i<=tot;++i)G[pa[i]].push_back(i);
dfs(1);
for(int i=1;i<=tot;++i)siz[i]=t?f[i]:(f[i]=1);
f[1]=siz[1]=0;
dfs2(1);
if(k>siz[1]){
puts("-1");
return 0;
}
query(1,k);
return 0;
}