• 【题解】字母 (letter)


    【题解】字母 (letter)

    没有传送门。

    【题目描述】

    给定一个长为 (n) 字符串 ( ext{S}),现取出 ( ext{S}) 的所有子串,并按字典序从小到大排序,然后将这些排好序的字符串首尾相接,记为字符串 ( ext{T})。共有 ( ext{Q}) 次询问,每次询问 ( ext{T}) 中的第 ( ext{K}) 个字符。

    操作强制在线,每次询问给出两个正整数 ( ext{P,M}),设 ( ext{G}) 为之前所有询问答案的 ( ext{ASCII}) 码之和(初始为 (0)),则对于该次询问:( ext{K}=( ext{P} imes ext{G}) mod ext{M})

    【输入格式】

    第一行输入一个字符串 ( ext{S}),第二行一个正整数 ( ext{Q})

    接下来 ( ext{Q}) 行,每行两个正整数 ( ext{P,M})

    【输出格式】

    对于每次询问,输出一行一个字符表示答案。

    【输入输出样例】

    样例输入:
    caaacacbbbababba
    6
    3 101
    16 101
    21 41
    22 129
    33 131
    40 100
    
    样例输出:
    a
    a
    a
    b
    c
    a
    

    【数据范围】

    (100\%:) (1 leqslant ext{M} leqslant | ext{T}|, 1 leqslant ext{P} leqslant 10^9)( ext{S}) 中仅包含小写字母。

    测试点 字符串 ( ext{S}) 的长度 (n) 询问的次数 ( ext{Q})
    (1) (1 leqslant n leqslant 50) (1 leqslant ext{Q} leqslant 200000)
    (2,3) (1 leqslant n leqslant 2000) (1 leqslant ext{Q} leqslant 20000)
    (4,5) (1 leqslant n leqslant 200000) (1 leqslant ext{Q} leqslant 10)
    (6,10) (1 leqslant n leqslant 200000) (1 leqslant ext{Q} leqslant 200000)

    【分析】

    鬼畜题见多了后遇到这种码农题感觉开心的要死。

    看到有 “所有子串” 这样的字样,基本上就是三种后缀算法实锤了,(std) 给的是后缀数组的做法,但个人认为这道题用后缀树要简单一些。

    首先是对所有的子串进行排序,明显为后缀树的板子操作。用 ( ext{SAM}) 构造好 ( ext{Parent}) 树,顺序遍历得到的节点序列 (id) 即可满足字典序从小到大,且对于同一节点中保存的子串,其长度越小字典序就越小。

    在建 ( ext{SAM}) 的时候记录下 (maxlen,siz) ((|{endpos}|))(pos) (({endpos}) 中的任意一个 ()),则 ( ext{SAM}) 中的一个保存信息的节点 (x) 所囊括的子串有:某一个前缀 (prefix)(len) 个后缀,长度分别为 (minlen(x),minlen(x)+1...maxlen(x)),其中 (len=maxlen(x)-minlen(x)+1)。所以 (x) 中所囊括的字符个数为:(cnt[x]=siz[x]sum_{i=minlen(x)}^{maxlen(x)}i)

    (S[n]=sum_{i=1}^{n}cnt[id[i]]),每次查询在 (S) 中二分找到答案所在的节点 (x),然后在 ([minlen(x),maxlen(x)]) 中二分找到答案所在的子串长度,最后用 ( ext{K}) 对该子串长度取模即可得到答案的位置(取模是因为该子串可能会出现多次)。

    后缀算法的细节考虑通常都很复杂,这道题也不例外,思维难度不高,但实现起来非常麻烦,考试时码了一个小时 ( ext{code}),然后又 ( ext{debug}) 半小时,算上写暴力和对拍的时间,前前后后花了接近三个小时。。。

    时间复杂度为:(O(n+qlogn)),实测比后缀数组的 (O((n+q)logn)) 快了好几倍。

    【Code】

    #include<algorithm>
    #include<cstring>
    #include<cstdio>
    #define LL long long
    #define Re register int
    using namespace std;
    const int N=4e5+10;
    int n,x,y,T;LL P,M,G,K;char s[N>>1];
    inline void in(Re &x){
        int f=0;x=0;char c=getchar();
        while(c<'0'||c>'9')f|=c=='-',c=getchar();
        while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
        x=f?-x:x;
    }
    struct Sakura2{
        int O,last,pos[N],siz[N],link[N],maxlen[N],trans[N][26];
        //siz[x]: 节点x的出现次数 (endpos的大小)
        //link[x]: x在parent树上的父亲
        //pos[x]: 节点x某一次出现位置 (用于构建后缀树中的压缩边link[x]->x),
        ///翻转后的s中 倒序遍历pos[x]-maxlen[link[x]] -> pos[x]-maxlen[x]+1得到的字符串 即为后缀树中的压缩边 link[x]->x
        inline void insert(Re ch,Re id){//SAM新建节点
            Re z=++O,p=last;pos[z]=id,siz[z]=1,maxlen[z]=maxlen[last]+1;
            while(p&&!trans[p][ch])trans[p][ch]=z,p=link[p];
            if(!p)link[z]=1;
            else{
                Re x=trans[p][ch];
                if(maxlen[p]+1==maxlen[x])link[z]=x;
                else{
                    Re y=++O;maxlen[y]=maxlen[p]+1,pos[y]=pos[x];
                    for(Re i=0;i<26;++i)trans[y][i]=trans[x][i];
                    while(p&&trans[p][ch]==x)trans[p][ch]=y,p=link[p];
                    link[y]=link[x],link[z]=link[x]=y;
                }
            }
            last=z;
        }
        int t,ID[N],to[N][26];LL S[N];
        //to[x][ch]: 后缀树的trans数组
        //ID[x]: 顺序遍历后缀树的第x个节点编号 
        //S[x]: 顺序遍历后缀树的前x个节点总共代表了多少个字符
        inline void dfs1(Re x){//遍历 SAM的parent树后缀树 获取siz
            for(Re i=0;i<26;++i)
                if(to[x][i])dfs1(to[x][i]),siz[x]+=siz[to[x][i]];
        }
        inline void dfs2(Re x){//遍历 SAM的parent树后缀树 获取节点顺序
            if(x!=1)ID[++t]=x;//没有储存信息的根节点1就不要了
            for(Re i=0;i<26;++i)if(to[x][i])dfs2(to[x][i]);
        }
        inline LL calc(Re L,Re R){return 1ll*(L+R)*(R-L+1)/2;}//计算从L加到R的等差数列 
        inline void build(){
            last=O=1;//根节点设为1
            for(Re i=1;i<=n/2;++i)swap(s[i],s[n-i+1]);//翻转原字符串
            for(Re i=1;i<=n;++i)insert(s[i]-'a',i);
            for(Re i=2;i<=O;++i)to[link[i]][s[pos[i]-maxlen[link[i]]]-'a']=i;//构建后缀树。
            //由于pos[x]是endpos[x]中的任意一个,所以获取边link[x]->x压缩掉的字符串信息时只能用pos[x],不能用pos[link[x]]
            dfs1(1),dfs2(1);//遍历 SAM的parent树后缀树
            for(Re i=1;i<=t;++i)x=ID[i],S[i]=S[i-1]+1ll*siz[x]*calc(maxlen[link[x]]+1,maxlen[x]);
        }
        inline char ask(LL K){
            Re l=1,r=t,x;
            while(l<r){//二分找到第一个大于等于K的位置
                Re mid=l+r>>1;
                if(S[mid]<K)l=mid+1;
                else r=mid;
            }
            x=ID[l],K-=S[l-1],l=maxlen[link[x]]+1,r=maxlen[x];
            while(l<r){//二分找到第一个大于等于K的位置
                Re mid=l+r>>1;
                if(1ll*siz[x]*calc(maxlen[link[x]]+1,mid)<K)l=mid+1;
                else r=mid;
            }
            K-=1ll*siz[x]*calc(maxlen[link[x]]+1,l-1),K=(K-1)%l+1;//注意取模的方式 
            return s[pos[x]-K+1];//注意方向:往左数第K位
        }
        char ans;
        inline void sakura(){
            build();
            while(T--)scanf("%lld%lld",&P,&M),K=P*G%M+1,G+=(ans=ask(K)),putchar(ans),puts("");
        }
    }T2;
    int main(){
        freopen("letter.in","r",stdin);
        freopen("letter.out","w",stdout);
        scanf("%s",s+1),n=strlen(s+1);in(T);
        T2.sakura();
        fclose(stdin);
        fclose(stdout);
    }
    
  • 相关阅读:
    每日立会2015-11-30
    Some Modern Softwares' drawbacks: User experience 12/29/2015
    Sprint 5 summary: UI 界面更新,Azure端部署和用户反馈分析 12/28/2015
    Daily Scrum 12/25/2015
    Daily Scrum 12/24/2015
    Daily Scrum 12/23/2015
    Daily Scrum 12/21/2015
    Daily Scrum 12/18/2015
    Daily Scrum 12/17/2015
    Performance standard (ALPHA release) 12/17/2015
  • 原文地址:https://www.cnblogs.com/Xing-Ling/p/12269575.html
Copyright © 2020-2023  润新知