【题解】字母 (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);
}
原文地址:https://www.cnblogs.com/Xing-Ling/p/12269575.html