bzoj3998 [TJOI2015]弦论(SAM)

Description

对于一个给定长度为N的字符串,求它的第K小子串是什么。

Input

 第一行是一个仅由小写英文字母构成的字符串S

第二行为两个整数T和K,T为0则表示不同位置的相同子串算作一个。T=1则表示不同位置的相同子串算作多个。K的意义如题所述。

Output

输出仅一行,为一个数字串,为第K小的子串。如果子串数目不足K个,则输出-1

Sample Input

aabc
0 3

Sample Output

aab

HINT

N<=5*10^5
T<2
K<=10^9

Source

[ Submit][ Status][ Discuss]



分析:

如果 T=0 ,后缀数组的做法很经典
按照所有后缀的字典序依次加入后缀,新加入一个后缀会产生“这个后缀的长度减去这个后缀的height值”这么多个子串
显然每次新产生的子串比之前产生的所有子串的字典序都大,那么就可以在前缀和上二分了
预处理 O(nlogn) ,单组询问 O(logn)

liu_runda大佬给出了 T=1 时的后缀数组解法
显然每个后缀都会产生这个后缀的长度这么多个子串,但这并没有什么用
我们不妨考虑把一个子串的多次出现全都算到这个子串在所有出现的后缀中字典序最小的那个后缀上
那么只需按照height从大到小合并,并查集维护一下就可以了
然后就可以前缀和+二分找出第k大的子串在是哪一个后缀所产生的字典序第几大的子串
这之后我们还需要支持查询某个后缀产生的子串中第k大的串是多少
于是强行又写了一遍按height合并所有后缀,预处理 O(nlogn)+ 单组询问 O(nlogn)


但是我就是想没事找事,感觉lrd的方法也不是很科学,而且我也不是很懂
SAM 怎么解决上面问题呢

首先当然是建一个 SAM 出来
SAM 的一个很有用性质:搜索 SAM 可以得到字符串所有的子串
而这些字符串是有字典序的,且互不重复

我们先统计一下每一个串出现了多少次: sizei

  • 对于不计重复的情况下: sizei =1
  • 对于计算重复的情况下:在 SAM 上, parent 代表的串一定是 son 的子串
    那么 son 的出现就意味着 parent 的出现, sizeparent+=sizeson

但是光一个 size 数组是不够的,我们还需要统计出从每个点出发后都有多少串

比如 S=ababababa 这个子串最后可以走到 abab,ababab ,于是 sum 为2
初值是所有结点的 sumi=sizeisumi=sumson

之后我们就可以在 SAM 上构造答案了
我们从 root 开始,从 a z遍历
如果 sum[ch[now][x]]>K ,即往 x 这个方向走后继不足K,那么这一位就应该大于 x
以此类推,构造答案

tip

感觉SAM和AC自动机,后缀数组完全不是一个级别的
首先ta的原理我就没有完全明白,应用起来不是特别理解

#include<cstdio>
#include<cstring>
#include<iostream>

using namespace std;

const int N=50003;
int T,K,len,root=1,sz=1,last=1,fa[N<<1],dis[N<<1],ch[N<<1][26];
int sum[N<<1],size[N<<1],pos[N<<1],c[N<<1];
char s[N];

void insert(int x)
{
    int now=++sz,pre=last;
    last=now; size[now]=1;
    dis[now]=dis[pre]+1;
    for (;pre&&!ch[pre][x];pre=fa[pre]) ch[pre][x]=now;

    if (!pre) fa[now]=root;
    else
    {
        int q=ch[pre][x];
        if (dis[q]==dis[pre]+1) fa[now]=q;
        else
        {
            int nows=++sz;
            dis[nows]=dis[pre]+1;
            memcpy(ch[nows],ch[q],sizeof(nows));
            fa[nows]=fa[q]; fa[q]=fa[now]=nows;
            for (;pre&&ch[pre][x]==q;pre=fa[pre]) ch[pre][x]=nows;
        }
    }
}

void work()
{
    for (int i=1;i<=sz;i++) c[dis[i]]++;
    for (int i=1;i<=len;i++) c[i]+=c[i-1];
    for (int i=1;i<=sz;i++) pos[c[dis[i]]--]=i;
    for (int i=sz;i>=1;i--)
        if (T) size[fa[pos[i]]]+=size[pos[i]];       //不同位置的相同子串算作多个
        else size[pos[i]]=1;
    size[root]=0;
    for (int i=sz;i>=1;i--)
    {
        sum[pos[i]]=size[pos[i]];
        for (int j=0;j<26;j++)
            if (ch[pos[i]][j])
                sum[pos[i]]+=sum[ch[pos[i]][j]];
    }
}

int main()
{
    scanf("%s",s+1); len=strlen(s+1);
    for (int i=1;i<=len;i++) insert(s[i]-'a');
    scanf("%d%d",&T,&K);

    work();

    if (K>sum[root]) {    
        printf("-1\n");
        return 0; 
    }
    int now=root;
    while ((K-=size[now])>0)   //size 当前状态的出现次数 
    {
        int p=0;
        while (K>sum[ch[now][p]]) K-=sum[ch[now][p++]];
        now=ch[now][p];       //按照字典序遍历 
        printf("%c",'a'+p);
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值