原题链接
算法概述
读完题目,显然可以用主席树做。
把所有的插入与撤销操作看成一个序列,按其操作顺序编号,则下标最多为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;
}