测试地址:Lexicographical Substring Search
题目大意:给定一个字符串,有Q个询问,每个询问字符串中字典序第Ki小的本质不同的子串。
做法:这几天学习了后缀自动机(Suffix Automaton,SAM),个人感觉除了clj的原论文,写得最好的就是这个,我就是看着这个学会后缀自动机的。里面最后讲的一道例题就是这道题,首先对字符串构建后缀自动机,然后对于每个点维护一个值s,指从这个点出发最多能找到的子串数,显然我们可以按原图的反向拓补序依次求出每一个点的s,所以我们可以用类似记忆化搜索的方法递推,搜索到没算过的点继续往下算,算过的就直接累加,当然从一个点出发能找到的子串也包括这个点自身,所以回来时记得+1。然后对于每个询问,从起始节点开始,按照字典序从小到大扫过每个儿子,如果K大于当前儿子能找到的子串数,则将K减去这个数,然后找下一个儿子,直到找到一个儿子使得从它出发能找到的子串数≥K,就输出这个儿子的字母,然后从这个儿子往下寻找。因为从一个点出发能找到的子串也包括这个点自身,所以每走过一个点,都要把K减去1,当K等于0的时候就结束。这样,总的时间复杂度就是O(NQ),可以通过。
我犯二的地方:第一次写想错了,把最后一个插入的点当成最后一个可以接受后缀的点......显然错了,要分清楚这两个概念。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
char s[100010];
int n,q,tot=0,in[200010]={0},last=0;
struct SAMnode
{
int pre,ch[30],step,s;
}nd[200010];
bool vis[200010]={0};
void extend(int c)
{
int p=last,q,np=++tot,nq;
nd[np].step=nd[p].step+1;
for(int i=0;i<=25;i++) nd[np].ch[i]=-1;
while(nd[p].ch[c]==-1&&p!=-1)
{
nd[p].ch[c]=np;
p=nd[p].pre;
}
if (p!=-1)
{
q=nd[p].ch[c];
if (nd[q].step==nd[p].step+1)
{
nd[np].pre=q;
}
else
{
nq=++tot;
nd[nq].pre=nd[q].pre;
for(int i=0;i<=25;i++) nd[nq].ch[i]=nd[q].ch[i];
nd[nq].step=nd[p].step+1;
nd[nq].s=1;
nd[q].pre=nq,nd[np].pre=nq;
while(p!=-1&&nd[p].ch[c]==q)
{
nd[p].ch[c]=nq;
p=nd[p].pre;
}
}
}
else nd[np].pre=0;
nd[np].s=1;
last=np;
}
void calc_s(int v)
{
for(int i=0;i<=25;i++)
if (nd[v].ch[i]!=-1)
{
if (!vis[nd[v].ch[i]]) calc_s(nd[v].ch[i]);
nd[v].s+=nd[nd[v].ch[i]].s;
}
vis[v]=1;
}
void build()
{
nd[0].pre=-1;nd[0].s=0;nd[0].step=0;
for(int i=0;i<=25;i++) nd[0].ch[i]=-1;
for(int i=0;i<n;i++) extend(s[i]-'a');
for(int i=0;i<=tot;i++)
if (nd[i].step==n) {vis[i]=1;break;}
calc_s(0);
}
void output(int a)
{
int v=0;
while(a)
{
for(int i=0;i<=25;i++)
if (nd[v].ch[i]!=-1)
{
if (a>nd[nd[v].ch[i]].s) a-=nd[nd[v].ch[i]].s;
else {v=nd[v].ch[i];printf("%c",i+'a');break;}
}
a--;
}
if (q) printf("
");
}
int main()
{
scanf("%s",s);
n=strlen(s);
build();
scanf("%d",&q);
while(q--)
{
int a;
scanf("%d",&a);
output(a);
}
return 0;
}