BZOJ 3998 弦论 (后缀自动机)

3998: [TJOI2015]弦论

Time Limit: 10 Sec Memory Limit: 256 MB

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

思路:
建出后缀自动机求K大的问题,先拓扑排序,然后递推出每个节点能转移到达的子串个数。(能够转移到达的子串都比它大)
这个题需要讨论一下,T=0时说明每个状态代表一个子串,T=1时每个节点的Parent树的子树中的节点数都是可以得到的子串数,所以需要累加。
考虑针对一个点u,它的26个子节点(如果有的话)每个点及其子树(它和所有它能转移到的子串)在子串的大小排列中都是一段连续的区间(排名是连续的),因为所有当前位为b的子串一定比所有当前位为a的子串大,不论后面接什么。
因此只要我们递推的维护了每个节点子树的大小(能转移到的子串个数,包括自己),就可以找到排名第k的子串了。所以可以在后缀自动机上递推,统计出所有子串的贡献。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int N = 5*1e5+10;

char s[N];
int n, T, root, last, sz;
int ch[N<<1][26], par[N<<1], sum[N<<1], num[N<<1], len[N<<1], b[N<<1], cnt[N];

void add_sam(int c){
    int cur = ++sz, p = last;
    len[cur] = len[p] + 1; num[cur] = 1;
    //num表示该子串在不同位置出现的次数
    for(; p&&!ch[p][c]; p=par[p]) ch[p][c] = cur;
    if( !p ) par[cur] = root;
    else{
        int q = ch[p][c];
        if(len[p]+1 == len[q]) par[cur] = q;
        else{
            int nq = ++sz;//虚拟节点
            memcpy(ch[nq], ch[q], sizeof(ch[nq]));
            len[nq] = len[p] + 1;
            par[nq] = par[q];
            par[cur] = par[q] = nq;
            for(; p&&ch[p][c]==q; p=par[p]) ch[p][c] = nq;
            }
        }
    last = cur;
}

void get_sum() {
    for(int i=1; i<=sz; i++) cnt[len[i]]++;
    for(int i=1; i<=n; i++) cnt[i] += cnt[i-1];
    for(int i=sz; i>=1; i--) b[cnt[len[i]]--] = i;//基数排序len 
    for(int i=sz; i>=1; i--)//len相当于deep 
        if( !T ) num[b[i]] = 1;//不统计不同位置相同串 
        else num[par[b[i]]] += num[b[i]];//统计不同位置相同串
    num[1] = 0;//
    for(int i=sz; i>=1; i--){
        int p = b[i]; sum[p] = num[p];//先计算自己(分情况看是否统计不同位置相同串) 
        for(int j=0; j<26; j++)//sum[p]表示p之后能转移到sum[p]个节点
        //p之后能转移到的节点都比p大(或相等),所以sum[p]也表示p是第sum[p]大的串 
            sum[p] += sum[ch[p][j]];
    }
}

int main() {
    scanf("%s", s); n = strlen(s);
    root = last = ++sz;//root为1 
    for(int i=0; i<n; i++) add_sam(s[i]-'a');
    int k;scanf("%d%d", &T, &k);
    get_sum();
    if(k > sum[1]){ printf("-1"); return 0; }
    int p = root;
    while(k > num[p]){//当前节点不满足条件 
        for(int c=0; c<26; c++)//遍历子节点 
            if( ch[p][c] ){
                if(sum[ch[p][c]] >= k){//此子树中有满足条件的点 
                    printf("%c", 'a' + c);//既然要进入此子树,那么一定有'a'+c 
                    k -= num[p]; p = ch[p][c];//进入子树中 
                    break;
                }
                else k -= sum[ch[p][c]];//跳过这一子树 
            }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值