数据结构录 之 主席树。
主席树(Chair Tree),一个神奇的数据结构。
实质上是函数式的线段树,或者可持久化的线段树。可持久化这个还比较好理解,Persistent Data Structures,看一下以上这篇文章就差不多明白为啥叫可持久化的的了。但是恕我太弱,对函数式编程实在理解不了多少,所以为啥是函数式的线段树,一时半会也不明白。 T_T
这里从ZOJ的 2112 这道题目讲起,可以先看一下题目是啥,就是求区间第K小,但是可以单点更新某个位置(一句话题意。)
下面我们来解一下这道题。。。
— 先想一下,要求第K小的话,要不就是有一种数据结构,能够直接得到第K个是几(然而并不知道有没有这样的数据结构。),要不就是能够对于某个数X,得到在区间有几个比他小的,这样的话二分就能知道区间第K小是多少了。这样的数据结构的话 线段树套二叉搜索树 就好(今天下午就试了一下。),但是现在说的是主席树。。。席树。。。树。
— 然后如果想知道比某个数X小的数有几个的话,用线段树维护所有数出现的频率就好了,这样直接就是询问1到X-1的频率和就好了,但是所有数太多,所以这里要先离散一下。
— 但是这样有问题啊,线段树维护的是哪里的所有数的出现频率啊,题目可是要求某个区间的第K小啊!
— 还没说完嘛,谁说就弄一颗线段树,你弄上N^2颗不就完了,把1-N的每个区间都弄一颗线段树,这样询问哪个区间找到那颗线段树,然后二分寻找第K小不就完了。
— N^2颗线段树?楼主你TM在逗我?N可是有50000啊!
— 好像。。。N^2确实不行哎,那么弄N颗行不,第 i 颗线段树记录的是原数组1- i 里面每个数出现的频率,反正是频率嘛,这样的话比如对于5这个数,你要求比5小的数在[3,7]这个区间里面出现的频率,直接就是 [1,7]-[1,2] 这两个区间出现的频率相减不就好了,反正是频率,满足相减性。
— 这样好像可以询问了呢。等等,楼主你又TM在逗我,还有单点更新操作啊好不,你有N颗线段树,每一颗都是记录的前缀啊,这样如果你要更新5的话,你不得把5,6,7。。。都更新了啊,这有法玩?
— 哦,对啊,还有更新操作啊,WTF,这怎么玩。
(这时,突然从天而降一道闪电,光照亮了。。。某个地方(编不出来了这里),然后,看到一道题)
“问,区间和怎么求,动态区间和怎么求?”
— 这个还不简单?区间和的话提前处理好所有前缀和,然后两个区间左右相减不就是了;动态的话,树状数组不就轻松搞定了。
— 等等,树状数组,动态区间和,对啊,干嘛每颗线段树记录的1- i 啊,为啥不能像树状数组一样,就想下面这个图这样记录这样一个区间啊。(为啥会有图出现?)
(盗图可耻。。。)
— 好像这样就可以了哎。。。这样的话每次单点更新只需要logN次就好了呢。。。
— 等等,楼主你还TM在骗我,你这一共搞了几颗线段树啊,你家电脑内存几T的啊能存下这么多线段树?
— 好像这是个问题呢,没事,不就是内存嘛,再买几(十)个内存条就好了。。。等等,刚刚开始不是还有一个链接吗,对,就是讲可持久化数据结构那个。
— 看了啊,里面哪里有线段树啊?
— 确实没有。。。不过可以借鉴里面的思想嘛。你看我们现在这线段树,如果有一个区间记录的是[1,5],另一个是[1,6]的话,就多了在第6个那个位置上的一个数而已,然后,这一个数只会改变线段树上面一个位置上的值而已,那么干嘛非得把整棵树都建好呢,浪费多可耻!是吧。直接利用前面那棵树,然后再自己建一个分支不就好了,反正线段树的话都是一个指针指向左右儿子嘛。
— 这样好像是省了很多空间时间呢,不过,好像你又在逗我了,这样的话后面的树用到了前面的树的一部分,这样如果对后面这棵树的某个位置的频率进行改变的话,前面那棵树不就也被改变了。。。
— 对哦,好像是有这么个问题。不过很简单嘛,如果要修改这棵树的某个分支的话,直接把原来的抛弃掉,再新建一个不就好了,反正这个分支就logN个节点而已嘛。原来那个也许还在被别人用的,也许没人用了,那就浪费了吧(是谁刚才还说浪费可耻的 = =),多买几个内存条就好了,就是这么任性。。。
int insert(int old,int val,int d) // 在old这个根指向的那棵树上更新一个新的分支,更新的值为val这个数的频率,频率加上d。(线段树的区间是数。) { int newRoot=Tcou++,ret=newRoot; // newRoot是新建的分支的根节点,并且保存下来用来做返回值。 int L=1,R=Hcou,M; BIT[newRoot]=BIT[old]+d; // 从这到下就是类似线段树的更新操作了,这一步是更新根节点维护的频率。(BIT数组维护的是数的区间的出现频率总和。) while(R>L) // 就像是线段树,一步步二分找到val所在的那个确切位置,然后沿途更新(新建)节点,而对另一半子树就直接指向原来的就好了。 { M=(L+R)>>1; if(val<=M) // 进入左子树,newRoot的左儿子被新建,然后右儿子指向原来那个,反正值都是一样的。 { lson[newRoot]=Tcou++; rson[newRoot]=rson[old]; newRoot=lson[newRoot]; old=lson[old]; R=M; } else // 进入右边,和上面一样。 { lson[newRoot]=lson[old]; rson[newRoot]=Tcou++; newRoot=rson[newRoot]; old=rson[old]; L=M+1; } BIT[newRoot]=BIT[old]+d; // 新节点的值是要维护的。 } return ret; }
那么更新(新建)一个新的分支就像是上面这样子了。这里可以来张图解释一下。。。圆圈里面上面是表示线段树所维护的数的区间,下面是维护的值,(注意线段树维护的是所有数,所以所有线段树都是一样的维护区间。),然后蓝色的表示在原来那颗线段树的基础上新建立的一颗线段树,你看,大半都用了之前那颗的东西。。。
(画的实在是不咋地好看。。。)
这样的话更新(插入)操作应该就搞定了,区间时间都可以接受,每次插入只要logN的时间来新建logN个节点就好了。。。
当然如果更新某个位置原来存在的值的话,就先把原来那个值的频率 -1 ,然后在对新值的频率 +1 就好了,这样其实是新建了两个分支啊,好浪费啊啊啊!
当然由于这些线段树是弄在树状数组上面的,那么更新一个值的话不能只更新一个区间,要更新logN个(还好不是太多。)。
那么更新的操作差不多就想是下面这样。
void add(int x,int val,int d) // 给树状数组上面的关于x的所有线段树进行insert操作。 { for(;x<=N;x+=lowbit(x)) TreeRoot[x]=insert(TreeRoot[x],val,d); // 在这颗线段树的基础上新建一个线段树(只是一些分支),这个线段树的根节点指向这个新线段树。 } void update(int up,int ut) // 更新up位置上数为ut。 { add(up,hash(num[up]),-1); // 把这个位置上面原来的数的频率减去1。 add(up,hash(ut),1); // 把ut这个数的频率加上1。 num[up]=ut; // num数组就是记录这个位置的数是多少的。 }
那么现在好像,已经解决了题目的一半了呢,那接下来就是另一半了。
也就是询问操作了,询问一个区间 ( ql , qr ) 里面第K小的数,这时候怎么搞呢,继续来说。
— 文章刚开始就说了,如果不能一下子得到区间第K小是几的话,我们可以试试二分,也就是对于每个数,只要能得到在这个区间里面有几个比他小的就好了,如果个数小于K的话,说明第K个在这个右边,不然就在左边,这样一步步缩小区间就好了。。。
— 那么怎么找呢?
— 先来对一个线段树说,比如说某颗维护特定区间的线段树,怎么找比某个数X小的个数呢,这个。。。好像不用说吧,直接询问线段树 1 - X-1 这个区间的频率和就好了,好吧,这就是线段树的操作嘛。。。
— 然后就是在一个区间上面一共有多少比X小的了,这时候就要用到树状数组了(要不然前面弄这个干什么?),先求出 1-qr 这个区间里面比X小的数的个数,然后求出 1 - ql-1这个区间里面的比X小的,相减就好了(满足相减性就是好。。。)。
— 那么怎么求 1-qr 这个区间里面的呢,树状数组嘛,不断减去 lowbit 就好了,对于每个节点的线段树统计一下就好了,这里不清楚的请自行(车)去看树状数组。
— 这样的话。。。好像询问也解决了。
询问的代码差不多就是下面这样。
int sum(int x,int L,int R) // 1-x这个区间(位置区间)里面,L到R上(数的区间)的这些数出现的次数。 { int ret=0; for(;x;x-=lowbit(x)) ret+=query_BIT(x,L,R); // 对x位置上的这颗线段树进行询问。 return ret; } int query(int ql,int qr,int K) // ql到qr这个区间(位置)上第K个是多少。 { int L=1,R=Hcou,M; while(R>L) // 二分。 { M=(L+R)>>1; temp=sum(qr,1,M)-sum(ql-1,1,M); // 1-M这个数的区间在ql到qr这个位置区间出现的次数。 if(temp>=K) // 第K个在1-M里面。 R=M; else // 在M+1到R里面。 { K-=temp; L=M+1; } } return L; // 第K个。 }
但是这样好像很麻烦呢,而且慢!还要在写一个线段树的区间询问的函数。
这里可以注意到一个地方,就是1-Hcou这个区间正好就是线段树所维护的区间方位,那么1 - M的话不正好就是线段树的左子树嘛,M+1 - R不正好就是右子树吗,这样干嘛还要在写个区间询问函数呢?直接返回某个子树的值不就好了。
所以可以添加一个use数组,记录现在到了线段树的哪颗子树的根了,代码如下:
int use[MaxN]; // 省事的一个数组,记录当前要询问的区间。 int sum(int x) // 求1-x的线段树的某个区间的连续和。 { int ret=0; for(;x;x-=lowbit(x)) ret+=BIT[lson[use[x]]]; return ret; } int query(int ql,int qr,int K) // ql到qr这个区间的第K小。 { int L=1,R=Hcou,M; int temp; for(int i=ql-1;i;i-=lowbit(i)) // 记录当前要找的区间。 use[i]=TreeRoot[i]; for(int i=qr;i;i-=lowbit(i)) use[i]=TreeRoot[i]; while(R>L) // 二分查找。 { M=(L+R)>>1; temp=sum(qr)-sum(ql-1); // 获得在ql到qr这个区间里面的1-M这些数的出现了多少次。 if(K<=temp) // 说明第K个在1-M里面找。 { for(int i=ql-1;i;i-=lowbit(i)) use[i]=lson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=lson[use[i]]; R=M; } else { for(int i=ql-1;i;i-=lowbit(i)) use[i]=rson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=rson[use[i]]; K-=temp; L=M+1; } } return L; // 这就是第K个。 }
怎么样,这样是不是更简单(个毛线)了,不过少了一个函数调用,应该差不多大概也许会快一点吧。。。
— 那么应该怎么建树呢?
— 建树,直接把刚开始的初始序列当做是N次更改操作,然后初始的话所有线段树就都是空树了。那么刚开始咱建一颗完整的空树给所有人来用,如果要更改某个位置的值的话,在这个空树上面建一个分支,就想上面说的 insert 那样,这样的话这颗空树也没人会改变,很浪费有没有!
那么建树的代码就像是下面这样。
int build_BIT(int L,int R) // 建一颗空的线段树。 { int root=Tcou++; BIT[root]=0; if(L!=R) { int M=(L+R)>>1; lson[root]=build_BIT(L,M); // 指向左儿子。 rson[root]=build_BIT(M+1,R); // 指向右儿子。 } return root; } void ChairTree_init(int num[]) // 初始化主席树。 { TreeRoot[0]=build_BIT(1,Hcou); //建空树。 for(int i=1;i<=N;++i) // 把所有线段树指向空树。 TreeRoot[i]=TreeRoot[0]; for(int i=1;i<=N;++i) // 对初始序列进行更新操作。 add(i,num[i],1); }
这样整棵主席树就差不多建好了。
完整代码如下。
// ━━━━━━神兽出没━━━━━━ // ┏┓ ┏┓ // ┏┛┻━━━━━━━┛┻┓ // ┃ ┃ // ┃ ━ ┃ // ████━████ ┃ // ┃ ┃ // ┃ ┻ ┃ // ┃ ┃ // ┗━┓ ┏━┛ // ┃ ┃ // ┃ ┃ // ┃ ┗━━━┓ // ┃ ┣┓ // ┃ ┏┛ // ┗┓┓┏━━━━━┳┓┏┛ // ┃┫┫ ┃┫┫ // ┗┻┛ ┗┻┛ // // ━━━━━━感觉萌萌哒━━━━━━ // Author : WhyWhy // Created Time : 2015年07月27日 星期一 23时43分28秒 // File Name : 2112_2.cpp #include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include <set> #include <map> #include <string> #include <math.h> #include <stdlib.h> #include <time.h> using namespace std; const int MaxN=50010+10010; const int MaxM=10005; const int MaxNode=2500010; int Tcou; int TreeRoot[MaxN]; int lson[MaxNode],rson[MaxNode],BIT[MaxNode]; int N; int num[MaxN]; int Hnum[MaxN],Hcou; // Hash相关,Hnum数组从1开始。 inline int hash(int x) { return lower_bound(Hnum+1,Hnum+Hcou+1,x)-Hnum; } inline int lowbit(int x) { return x&(-x); } int insert(int old,int val,int d) // 在old这个根指向的那棵树上更新一个新的分支,更新的值为val这个数的频率,频率加上d。(线段树的区间是数。) { int newRoot=Tcou++,ret=newRoot; // newRoot是新建的分支的根节点,并且保存下来用来做返回值。 int L=1,R=Hcou,M; BIT[newRoot]=BIT[old]+d; // 从这到下就是类似线段树的更新操作了,这一步是更新根节点维护的频率。(BIT数组维护的是数的区间的出现频率总和。) while(R>L) // 就像是线段树,一步步二分找到val所在的那个确切位置,然后沿途更新(新建)节点,而对另一半子树就直接指向原来的就好了。 { M=(L+R)>>1; if(val<=M) // 进入左子树,newRoot的左儿子被新建,然后右儿子指向原来那个,反正值都是一样的。 { lson[newRoot]=Tcou++; rson[newRoot]=rson[old]; newRoot=lson[newRoot]; old=lson[old]; R=M; } else // 进入右边,和上面一样。 { lson[newRoot]=lson[old]; rson[newRoot]=Tcou++; newRoot=rson[newRoot]; old=rson[old]; L=M+1; } BIT[newRoot]=BIT[old]+d; // 新节点的值是要维护的。 } return ret; } void add(int x,int val,int d) // 给树状数组上面的关于x的所有线段树进行insert操作。 { for(;x<=N;x+=lowbit(x)) TreeRoot[x]=insert(TreeRoot[x],val,d); // 在这颗线段树的基础上新建一个线段树(只是一些分支),这个线段树的根节点指向这个新线段树。 } void update(int up,int ut) // 更新up位置上数为ut。 { add(up,hash(num[up]),-1); // 把这个位置上面原来的数的频率减去1。 add(up,hash(ut),1); // 把ut这个数的频率加上1。 num[up]=ut; // num数组就是记录这个位置的数是多少的。 } int use[MaxN]; // 省事的一个数组,记录当前要询问的区间。 int sum(int x) // 求1-x的线段树的某个区间的连续和。 { int ret=0; for(;x;x-=lowbit(x)) ret+=BIT[lson[use[x]]]; return ret; } int query(int ql,int qr,int K) // ql到qr这个区间的第K小。 { int L=1,R=Hcou,M; int temp; for(int i=ql-1;i;i-=lowbit(i)) // 记录当前要找的区间。 use[i]=TreeRoot[i]; for(int i=qr;i;i-=lowbit(i)) use[i]=TreeRoot[i]; while(R>L) // 二分查找。 { M=(L+R)>>1; temp=sum(qr)-sum(ql-1); // 获得在ql到qr这个区间里面的1-M这些数的出现了多少次。 if(K<=temp) // 说明第K个在1-M里面找。 { for(int i=ql-1;i;i-=lowbit(i)) use[i]=lson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=lson[use[i]]; R=M; } else { for(int i=ql-1;i;i-=lowbit(i)) use[i]=rson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=rson[use[i]]; K-=temp; L=M+1; } } return L; // 这就是第K个。 } int build_BIT(int L,int R) // 建一颗空的线段树。 { int root=Tcou++; BIT[root]=0; if(L!=R) { int M=(L+R)>>1; lson[root]=build_BIT(L,M); // 指向左儿子。 rson[root]=build_BIT(M+1,R); // 指向右儿子。 } return root; } void ChairTree_init(int num[]) // 初始化主席树。 { TreeRoot[0]=build_BIT(1,Hcou); // 建空树。 for(int i=1;i<=N;++i) // 把所有线段树指向空树。 TreeRoot[i]=TreeRoot[0]; for(int i=1;i<=N;++i) // 对初始序列进行更新操作。 add(i,num[i],1); } void Hash_init() { sort(Hnum+1,Hnum+Hcou+1); Hcou=unique(Hnum+1,Hnum+Hcou+1)-Hnum-1; } void init() { Tcou=Hcou=0; } struct Query { bool type; int a,b,c; }q[MaxM]; int main() { //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); int T; char ts[10]; int M; scanf("%d",&T); while(T--) { scanf("%d %d",&N,&M); init(); for(int i=1;i<=N;++i) { scanf("%d",&num[i]); Hnum[++Hcou]=num[i]; } for(int i=1;i<=M;++i) { scanf("%s",ts); if(ts[0]=='Q') { q[i].type=0; scanf("%d %d %d",&q[i].a,&q[i].b,&q[i].c); } else { q[i].type=1; scanf("%d %d",&q[i].a,&q[i].b); Hnum[++Hcou]=q[i].b; } } Hash_init(); ChairTree_init(num); for(int i=1;i<=M;++i) { if(q[i].type) update(q[i].a,q[i].b); else printf("%d\n",Hnum[query(q[i].a,q[i].b,q[i].c)]); } } return 0; }
测试了样例之后发现对了,然后(欢欢喜喜)的交上去之后,然后。。。
居然 段错误 了,感觉应该没错啊,然会目测应该是数组开小了,然后开大之后发现。。。MLE了,WTF!这怎么玩?也就是说用主席树的话空间根本不够,果然浪费可耻啊!
。。。
(这时,突然从天而降一道闪电(这好像在哪里听过),照亮了。。。那遥远的地方2333333。)
— 如果是静态第K小的话,每次建前缀的线段树就好了,只需要N个新的分支,多省空间啊是吧?你看这个动态的,真够麻烦的,N个数就要NlogN个分支。
— 等一下,静态的话省空间是吧,那么干嘛不能用静态的呢是吧。
— 对于初始序列干嘛要当做是N个插入操作啊,这样多浪费空间啊,把他们单独弄成一组前缀的线段树,然后对于之后的每次修改操作再弄到树状数组里面不就好了,这样的话求区间频率就是树状数组那一部分和前缀和那一部分的总和不就是了嘛。总之要想省空间,就要尽可能减少分支新建的个数。。。
然后修改之后的代码如下。
// ━━━━━━神兽出没━━━━━━ // ┏┓ ┏┓ // ┏┛┻━━━━━━━┛┻┓ // ┃ ┃ // ┃ ━ ┃ // ████━████ ┃ // ┃ ┃ // ┃ ┻ ┃ // ┃ ┃ // ┗━┓ ┏━┛ // ┃ ┃ // ┃ ┃ // ┃ ┗━━━┓ // ┃ ┣┓ // ┃ ┏┛ // ┗┓┓┏━━━━━┳┓┏┛ // ┃┫┫ ┃┫┫ // ┗┻┛ ┗┻┛ // // ━━━━━━感觉萌萌哒━━━━━━ // Author : WhyWhy // Created Time : 2015年07月27日 星期一 23时43分28秒 // File Name : 2112_2.cpp #include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include <set> #include <map> #include <string> #include <math.h> #include <stdlib.h> #include <time.h> using namespace std; const int MaxN=50010+10010; const int MaxM=10005; const int MaxNode=2600010; int Tcou; int TreeRoot[MaxN]; int lson[MaxNode],rson[MaxNode],BIT[MaxNode]; int BaseTree[MaxN]; // 对于初始序列的前缀建的树的根。 int N; int num[MaxN]; int Hnum[MaxN],Hcou; // Hash相关,Hnum数组从1开始。 inline int hash(int x) { return lower_bound(Hnum+1,Hnum+Hcou+1,x)-Hnum; } inline int lowbit(int x) { return x&(-x); } int insert(int old,int val,int d) // 在old这个根指向的那棵树上更新一个新的分支,更新的值为val这个数的频率,频率加上d。(线段树的区间是数。) { int newRoot=Tcou++,ret=newRoot; // newRoot是新建的分支的根节点,并且保存下来用来做返回值。 int L=1,R=Hcou,M; BIT[newRoot]=BIT[old]+d; // 从这到下就是类似线段树的更新操作了,这一步是更新根节点维护的频率。(BIT数组维护的是数的区间的出现频率总和。) while(R>L) // 就像是线段树,一步步二分找到val所在的那个确切位置,然后沿途更新(新建)节点,而对另一半子树就直接指向原来的就好了。 { M=(L+R)>>1; if(val<=M) // 进入左子树,newRoot的左儿子被新建,然后右儿子指向原来那个,反正值都是一样的。 { lson[newRoot]=Tcou++; rson[newRoot]=rson[old]; newRoot=lson[newRoot]; old=lson[old]; R=M; } else // 进入右边,和上面一样。 { lson[newRoot]=lson[old]; rson[newRoot]=Tcou++; newRoot=rson[newRoot]; old=rson[old]; L=M+1; } BIT[newRoot]=BIT[old]+d; // 新节点的值是要维护的。 } return ret; } void add(int x,int val,int d) // 给树状数组上面的关于x的所有线段树进行insert操作。 { for(;x<=N;x+=lowbit(x)) TreeRoot[x]=insert(TreeRoot[x],val,d); // 在这颗线段树的基础上新建一个线段树(只是一些分支),这个线段树的根节点指向这个新线段树。 } void update(int up,int ut) // 更新up位置上数为ut。 { add(up,hash(num[up]),-1); // 把这个位置上面原来的数的频率减去1。 add(up,hash(ut),1); // 把ut这个数的频率加上1。 num[up]=ut; // num数组就是记录这个位置的数是多少的。 } int use[MaxN]; // 省事的一个数组,记录当前要询问的区间。 int sum(int x) // 求1-x的线段树的某个区间的连续和。 { int ret=0; for(;x;x-=lowbit(x)) ret+=BIT[lson[use[x]]]; return ret; } int query(int ql,int qr,int K) // ql到qr这个区间的第K小。 { int L=1,R=Hcou,M; int temp; int BaseL=BaseTree[ql-1],BaseR=BaseTree[qr];// 记录对初始序列的线段树当前所要查询的位置。 for(int i=ql-1;i;i-=lowbit(i)) // 记录当前要找的区间。 use[i]=TreeRoot[i]; for(int i=qr;i;i-=lowbit(i)) use[i]=TreeRoot[i]; while(R>L) // 二分查找。 { M=(L+R)>>1; temp=sum(qr)-sum(ql-1); // 获得在ql到qr这个区间里面的1-M这些数的出现了多少次。 temp+=BIT[lson[BaseR]]-BIT[lson[BaseL]];// 再加上初始序列线段树的。 if(K<=temp) // 说明第K个在1-M里面找。 { for(int i=ql-1;i;i-=lowbit(i)) use[i]=lson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=lson[use[i]]; BaseL=lson[BaseL]; BaseR=lson[BaseR]; R=M; } else { for(int i=ql-1;i;i-=lowbit(i)) use[i]=rson[use[i]]; for(int i=qr;i;i-=lowbit(i)) use[i]=rson[use[i]]; BaseL=rson[BaseL]; BaseR=rson[BaseR]; K-=temp; L=M+1; } } return L; // 这就是第K个。 } int build_BIT(int L,int R) // 建一颗空的线段树。 { int root=Tcou++; BIT[root]=0; if(L!=R) { int M=(L+R)>>1; lson[root]=build_BIT(L,M); // 指向左儿子。 rson[root]=build_BIT(M+1,R); // 指向右儿子。 } return root; } void ChairTree_init(int num[]) // 初始化主席树。 { TreeRoot[0]=build_BIT(1,Hcou); // 建空树。 for(int i=1;i<=N;++i) // 把所有线段树指向空树。 { TreeRoot[i]=TreeRoot[0]; BaseTree[i]=insert(BaseTree[i-1],hash(num[i]),1); } } void Hash_init() { sort(Hnum+1,Hnum+Hcou+1); Hcou=unique(Hnum+1,Hnum+Hcou+1)-Hnum-1; } void init() { Tcou=Hcou=0; } struct Query { bool type; int a,b,c; }q[MaxM]; int main() { //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); int T; char ts[10]; int M; scanf("%d",&T); while(T--) { scanf("%d %d",&N,&M); init(); for(int i=1;i<=N;++i) { scanf("%d",&num[i]); Hnum[++Hcou]=num[i]; } for(int i=1;i<=M;++i) { scanf("%s",ts); if(ts[0]=='Q') { q[i].type=0; scanf("%d %d %d",&q[i].a,&q[i].b,&q[i].c); } else { q[i].type=1; scanf("%d %d",&q[i].a,&q[i].b); Hnum[++Hcou]=q[i].b; } } Hash_init(); ChairTree_init(num); for(int i=1;i<=M;++i) { if(q[i].type) update(q[i].a,q[i].b); else printf("%d\n",Hnum[query(q[i].a,q[i].b,q[i].c)]); } } return 0; }
然后终于(TM)过了。。。真是醉了。。。
通过这个题差不多也就说了说主席树了,感觉主席树就是一群线段树而已,然后每次充分利用之前的线段树,只新建一个分支。
这个题的话是维护了每个数出现的次数,感觉可以维护的东西很多,只要能够借用之前的线段树就好。
突然觉得用处好大,嗯,确实好大。
作为一个菜鸟,主席树到目前也只是学了一些皮毛,然后像个逗比(就是逗比)一样写了这个,来加深一下印象。