原题链接
算法概述
读完题目,显然可以用主席树做。
把所有的插入与撤销操作看成一个序列,按其操作顺序编号,则下标最多为1~n(因为有查询操作的存在,下标实际上不可能到n)。
在下标上建立主席树。
每个节点记录其所代表的下标区间内已经插入的字母个数。特别地,叶子节点需要额外记录其下标所对应的操作插入的字母。鉴于本人的字符恐惧症,字母信息直接用数字1~26代替。
对于插入操作,直接在当前这个操作编号上插入一个字母即可,同时维护区间内字母个数。
对于撤销操作,假设撤销x步,那就是直接回到x步前的历史版本。
对于查询操作,假设是查询当前版本的第k个字母,基于线段树的二分结构,对当前所查询到的节点p来说,设其左儿子为L,右儿子为R,节点信息中字母个数为size。若k<=L.size,则递归左儿子,查询第k个字母;否则递归右儿子,查询第k-L.size个字母。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=1e5+10; struct Node{ int l,r; int siz; int dat; #define l(p) tree[p].l #define r(p) tree[p].r #define siz(p) tree[p].siz #define dat(p) tree[p].dat }tree[N*21];int idx; int root[N]; int n; int build(int l,int r) { int p=++idx; if(l==r)return p; int mid=l+r>>1; l(p)=build(l,mid); r(p)=build(mid+1,r); return p; } int insert(int now,int l,int r,int pos,int ch) { int p=++idx; tree[p]=tree[now]; if(l==r) { dat(p)=ch; siz(p)=1; return p; } int mid=l+r>>1; if(pos<=mid)l(p)=insert(l(now),l,mid,pos,ch); else r(p)=insert(r(now),mid+1,r,pos,ch); siz(p)=siz(l(p))+siz(r(p)); return p; } int query(int p,int l,int r,int x) { if(l==r)return dat(p); int mid=l+r>>1; if(x<=siz(l(p)))return query(l(p),l,mid,x); else return query(r(p),mid+1,r,x-siz(l(p))); } int main() { scanf("%d",&n); int cnt=0; for(int i=1;i<=n;i++) { char tp[2]; scanf("%s",tp); if(tp[0]=='T') { cnt++; char x;cin>>x; root[cnt]=insert(root[cnt-1],1,n,cnt,x-'a'+1); } else if(tp[0]=='U') { cnt++; int x;scanf("%d",&x); root[cnt]=root[cnt-1-x]; } else { int x;scanf("%d",&x); char ans=query(root[cnt],1,n,x)-1+'a'; printf("%c ",ans); } } return 0; }