12.10日记
主席树
- P2617(带修主席树模板):给定n个数的序列,查询区间第k小+单点修改。本题非强制在线。
思路:其实主席树也只是一种复用重复空间的思想,并不是一种特定的数据结构。相反,他和动态开点有不少相似之处。甚至说,普通的线段树就是一种特殊的抽象化线段树。我感觉做了这么多线段树的题目,这是我能总结出来的最好的版本了。
首先是线段树的结构体化:
struct Tree{
int l,r,val;
Tree(int a=0,int b=0,int c=0):l(a),r(b),val(c){}
}v[M*200];
(l)表示左儿子的下标,(r)表示右儿子的下标,(val)表示线段树每个节点的权值。
真正的线段树正是如此。只不过传统上来讲,为了方便初学者理解,大家都是从(l=id*2,r=id*2+1)这种最简单的线段树开始讲起。实际上左儿子和右儿子并不一定得是固定的二倍关系,甚至都不一定得是两个儿子,你写三个儿子,变成l,m,r,然后操作的时候多搞一下,可能会更优(我口胡的),只不过写起来比较麻烦,而且最多就是差了个常数。
那么如果父亲和儿子不是固定关系的话,那么该怎么确定儿子的下标呢?很简单,就是
- 动态开点。每次走到一个节点,要去进行操作之前(operate/query),首先先判断一下这个节点是否存在(是否等于0),如果为0,那之后的就不用operate了,对于query,直接return 0就行了(对于求和与单点查询),少了一堆常数有木有!?
- 连到已经有的节点上,比如说主席树。那么这个儿子更深的部分你就不用管了,又少了一堆常数。
这种思想太有用了,简单来讲就是,对于线段树,我只需要知道l,r儿子的节点编号,至于是不是两倍或者从小到大,无所谓。
摆脱了这种思想的桎梏,那么理解主席树就相对容易了吧,实际上因为单点修改每次只会修改一条链,所以只会改变(log n)个节点,所以新建一颗线段树的时候,很多节点都可以直接再连到之前的树上,不需要自己再新建,达到了复用已有信息,防炸空间的目的。
但如果是区间修改就不能用主席树了,因为改变的节点远超(log n)个,自然就无法达到复用的目的。
那么如果想区间修改该怎么办呢?利用数据结构的关键思想之一:区间加减通过建立差分数组,以变成单点加减,这样区间和就变成了对应节点的数值,再套一个区间和就能求回原来的区间和了。复杂度的话,只是在修改和维护的时候多了一倍的常数,渐进复杂度应该是一致的。(以上均为个人口胡,我还没写过)。
最后总结一下常见(其实就是平衡树)的基本操作(插入,删除,找排名第k,求k的排名,找k前驱,找k后继)的实现方式(基本操作就是operate(改个数),query_num(查询某个数有几个),query_rk(查询排名第k是谁),query_sa(求k的排名),后面两个找前驱后继可以用前面几个操作实现):(回头再总结吧)
-
静态整体:直接sort
-
动态整体(带修):
-
动态整体(带修+强制在线):权值线段树
-
静态区间:主席树
-
动态区间(带修):
-
动态区间(带修+强制在线):树状数组,每个节点都是一颗权值线段树,动态开点。(nlog^2n)。
(如果你还不懂权值线段树是啥建议翻翻前几天我的日记——)
目前我还不会动态的离线做法,只会强制在线的大常数做法。看了题解之后感觉应该离线就是套一层CDQ?以达到顶替高级数据结构+减常数的作用?
好了说了那么多废话,该说说这个题该怎么做了。
这个题属于动态区间(带修),可以离线(整体二分),洛谷题解中也有,但我不会CDQ。所以就讲讲在线做法。
首先思考,对于一个区间,如果我得到了这个区间中所有数构成的权值线段树,那么这道题就变成了动态整体了,直接在权值线段树上写函数搜索就可以了。
那么怎么样每次快速得到指定区间对应的权值线段树?
考虑到权值线段树本身具有加法结合律,因为每个节点表示的是数的个数,显然嘛。所以可以外面套一个树状数组,记录对应区间的权值线段树的和。由区间证明,任何一个区间最多只需要被分成(log n)个小区间,所以得到指定区间对应的权值线段树上的一个节点的值,复杂度是(O(log n)),方法就是把这个区间对应的那n个小区间的权值线段树的,对应这个节点上的值都加起来。
所以相当于一共nlogn颗权值线段树,空间肯定炸,所以必须先离散化,再动态开点,最大化省空间(虽然你不离散化其实也能过)。
那么每次修改,相当于对(O(log n))颗线段树进行单点修改,所以复杂度是(log^2n)的。
萌新:我哪知道要修改哪logn颗?
我:树状数组啊,每次直接lowbit就行了。
所以建议用for形式的树状数组,这样很容易理解:
for(int i=x;i;i-=lowbit(i))
操作
这样的话,所有的i就是query(1,x)前缀和时,这个区间被分成的那(O(log n))个区间的下标(或者说是权值线段树的编号)。每次就这么写就行了,不用动脑子,也不用想原理,多好。
每次查询,需要注意,本质上你还是在一颗权值线段树上进行操作,只不过这个线段树在内存空间中你并没有真正存,只知道他可以拆成logn颗你已经存过的树的和。所以对于这颗“虚拟”的权值线段树,每个节点的值都要用logn的时间去加起来,对应的和就是这个权值线段树这个节点的值。只能在用的时候单次查询。
那他的左右儿子呢?实际上你还是没有存,和刚刚一样,只知道拆成了logn颗线段树对应节点的儿子。所以……每次走之前,都要用一个now数组存一下当前每颗线段树走到了哪里,如果要走左儿子,那么logn颗线段树都要走到左儿子,这个操作也是logn的。如果有动态开点,那么很有可能走到一定深度的时候有些儿子就全0了,那么就可以省一些时间,但要注意,一次复杂度就是logn,实际上能省很多(但我这次代码里没写)。
那么这个题就结束了,看代码吧。树状数组和权值线段树的pos,id等等真的很容易混。
(平衡树?那是什么?我只会权值线段树谢谢)
#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define db(x) cout<<#x<<":"<<x<<endl;
const int M=1e5+20;
vector<int> lsh;
unordered_map<int,int> rev;
struct Opt{
char op[2];
int l,r,k;
Opt():l(0),r(0),k(0){
op[0]=' 00';
}
}opt[M];
struct Tree{
int l,r,val;
Tree(int a=0,int b=0,int c=0):l(a),r(b),val(c){}
}v[M*200];
int a[M],cnt,len,now[M*2];
inline int lowbit(int x){return x&(-x);}
void operate(int &id,int l,int r,int pos,int x){
if (!id)
id=++cnt;
v[id].val+=x;
if (l==r)
return;
if (pos<=mid)
operate(v[id].l,l,mid,pos,x);
else
operate(v[id].r,mid+1,r,pos,x);
}
inline void BIToperate(int id,int pos,int k){
while(id<=len)
operate(id,1,len,pos,k),id+=lowbit(id);
}
int query_rk(int l,int r,int ql,int qr,int k){
int Lnum=0;
if (l==r){
for(int i=qr;i;i-=lowbit(i))
now[i]=i;
for(int i=ql-1;i;i-=lowbit(i))
now[i]=i;
return l;
}
for(int i=qr;i;i-=lowbit(i))
Lnum+=v[v[now[i]].l].val;
for(int i=ql-1;i;i-=lowbit(i))
Lnum-=v[v[now[i]].l].val;
if (Lnum>=k){
for(int i=qr;i;i-=lowbit(i))
now[i]=v[now[i]].l;
for(int i=ql-1;i;i-=lowbit(i))
now[i]=v[now[i]].l;
return query_rk(l,mid,ql,qr,k);
}
else{
for(int i=qr;i;i-=lowbit(i))
now[i]=v[now[i]].r;
for(int i=ql-1;i;i-=lowbit(i))
now[i]=v[now[i]].r;
return query_rk(mid+1,r,ql,qr,k-Lnum);
}
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]),lsh.push_back(a[i]);
for(int i=1;i<=m;++i){
scanf("%s",opt[i].op);
if (opt[i].op[0]=='Q')
scanf("%d%d%d",&opt[i].l,&opt[i].r,&opt[i].k);
else
scanf("%d%d",&opt[i].l,&opt[i].r),lsh.push_back(opt[i].r);
}
sort(lsh.begin(),lsh.end());
len=unique(lsh.begin(),lsh.end())-lsh.begin();
for(int i=0;i<len;++i)
rev[lsh[i]]=i+1;
for(int i=1;i<=len;++i)
now[i]=i;
cnt=len;
for(int i=1;i<=n;++i)
BIToperate(i,rev[a[i]],1);
for(int i=1;i<=m;++i)
if (opt[i].op[0]=='Q')
printf("%d
",lsh[query_rk(1,len,opt[i].l,opt[i].r,opt[i].k)-1]);
else
BIToperate(opt[i].l,rev[a[opt[i].l]],-1),a[opt[i].l]=opt[i].r,BIToperate(opt[i].l,rev[a[opt[i].l]],1);
return 0;
}
总结
今天一天实验室,实验做的并不好,大家都不是很高兴。当然也没有什么时间写题,只做了这一个。但是写的过程中真的收获了很多,写的第二道树套树。关键还是要想明白,想明白,写代码也不会出问题,就更不需要调试了。
那么香港H题也就会了,区间查询的平衡树(二逼平衡树)也就可以A穿了。所以这么看,香港打铜尾是说明我自己真的菜。
明日计划
- 二逼平衡树P3380
- 香港H题,过不了的话就学一下离线做法。
- FHQ-treap和替罪羊树就先不学了吧,感觉学了也练不好,就最后学一下万能的CDQ分治,多巩固一下之前的字符串和数学,就先这样吧。