【题解】字母 (letter)

【题解】字母 (letter)

没有传送门。

【题目描述】

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

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

【输入格式】

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

接下来 \(\text{Q}\) 行,每行两个正整数 \(\text{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 \text{M} \leqslant |\text{T}|, 1 \leqslant \text{P} \leqslant 10^9\)\(\text{S}\) 中仅包含小写字母。

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

【分析】

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

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

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

在建 \(\text{SAM}\) 的时候记录下 \(maxlen,siz\) \((|\{endpos\}|)\)\(pos\) \((\{endpos\}\) 中的任意一个 \()\),则 \(\text{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)]\) 中二分找到答案所在的子串长度,最后用 \(\text{K}\) 对该子串长度取模即可得到答案的位置(取模是因为该子串可能会出现多次)。

后缀算法的细节考虑通常都很复杂,这道题也不例外,思维难度不高,但实现起来非常麻烦,考试时码了一个小时 \(\text{code}\),然后又 \(\text{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);
}
posted @ 2020-02-06 17:48  辰星凌  阅读(402)  评论(0编辑  收藏  举报