【洛谷】P3975 [TJOI2015]弦论(SAM)
题意
对于一个给定长度为 \(N\) 的字符串,求它的第 \(K\) 小子串。
数据范围
\(1\leq n \leq 10^5\),\(0\leq t \leq 1\),\(1\leq k \leq 10^9\)。
\(t\) 为 \(0\) 则表示不同位置的相同子串算作一个,\(t\) 为 \(1\) 则表示不同位置的相同子串算作多个。
思路
考虑后缀自动机(下文默认已经知晓 SAM 的基本性质)。
根据题意,当 \(t=0\) 时,SAM 上的每个节点的 \(Endpos\) 集合大小(该集合代表该节点对应的所有子串在原串中所有出现的位置)都为 \(1\)。反之则可以根据 parent 树的性质从下往上递推。定义 \(i\) 点的 \(Endpos\) 集合大小为 \(siz[i]\)。
定义 \(sum[i]\) 表示经过 \(i\) 节点的子串数目(严格来说并不是,往后看就明白了)。即当前节点的 \(siz\) 加上以当它为前缀的所有节点的 \(siz\)。可以在 SAM 的 DAG 上逆序递推。需要注意的是,这里不需要考虑 \(i\) 节点对应的所有子串,因为接下来查询的时候根据字典序的性质是从前往后推的,也就是一个前缀不断往后扩展,而不是后缀往前扩展(举个简单的例子,当前节点对应的子串集合为 {\(aabb,abb,bb\)},那么后面查询到这里时可能是从 \(aab\) 过来的,也可能是从 \(b\) 过来的,此时就与对应的其他子串无关了)。
查询的时候就是从根节点往后推,根据当前节点的 \(siz\) 和 \(sum\) 判断是否要继续往后推,具体实现可以见代码,这部分放在代码里将更清晰。
code
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e6+10;
#define LL long long
struct SAM{int ch[26],len,fa;}node[N];
int c[N],last=1,tot=1,t,k,num[N];LL siz[N],sum[N]; char s[N];
void extend(int c)
{
int p=last,np=last=++tot;node[np].len=node[p].len+1;siz[np]=1;
while(p&&!node[p].ch[c]) node[p].ch[c]=np,p=node[p].fa;
if(!p) node[np].fa=1;
else
{
int q=node[p].ch[c];
if(node[p].len+1==node[q].len) node[np].fa=q;
else
{
int nq=++tot;node[nq]=node[q];node[nq].len=node[p].len+1;
node[q].fa=node[np].fa=nq;
while(p&&node[p].ch[c]==q) node[p].ch[c]=nq,p=node[p].fa;
}
}
}
void print(int now,int k) //k表示当前还要查询的第k小子串
{
if(k<=siz[now]) return ; //如果当前节点出现次数不小于k,就代表查询子串就是当前已经走到的子串,不需要往后推
k-=siz[now];
for(int i=0;i<26;i++) //根据字典序的性质, 从小到大枚举当前位字符
{
int t=node[now].ch[i];if(!t) continue;
if(sum[t]<k) k-=sum[t];//有点类似于splay查询右子树时减去左子树的size
else {printf("%c",i+'a');print(t,k);return ;}
}
}
int main()
{
scanf("%s%d%d",s+1,&t,&k);for(int i=1;s[i];i++) extend(s[i]-'a');
for(int i=1;i<=tot;i++) c[node[i].len]++;//求siz和sum数组可以按照这里的基数排序的方式,也可以直接建图dfs
for(int i=1;i<=tot;i++) c[i]+=c[i-1];
for(int i=1;i<=tot;i++) num[c[node[i].len]--]=i;
for(int i=tot;i>=1;i--) siz[node[num[i]].fa]+=siz[num[i]];
for(int i=1;i<=tot;i++) t?sum[i]=siz[i]:siz[i]=sum[i]=1;
siz[1]=sum[1]=0;//需要注意,根节点没有任何信息,下面的sum[1]是判断无解
for(int i=tot;i>=1;i--)
for(int j=0;j<26;j++)
sum[num[i]]+=sum[node[num[i]].ch[j]];
if(sum[1]<k) puts("-1");
else print(1,k),puts("");
return 0;
}```