• 【日记】12.10


    12.10日记

    主席树

    1. 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穿了。所以这么看,香港打铜尾是说明我自己真的菜。

    明日计划

    1. 二逼平衡树P3380
    2. 香港H题,过不了的话就学一下离线做法。
    3. FHQ-treap和替罪羊树就先不学了吧,感觉学了也练不好,就最后学一下万能的CDQ分治,多巩固一下之前的字符串和数学,就先这样吧。
  • 相关阅读:
    Place the Robots 需要较强的建图能力
    Channel Allocation 贪心涂色
    A Simple Math Problem 矩阵打水题
    按钮的高亮状态的颜色
    设置UITableViewCell选中颜色但是无效
    常用的忽略警告
    UIButton按钮的高亮状态颜色
    字节的不同单位间的转换
    通过颜色绘制图片UIImage
    头像裁剪功能的实现
  • 原文地址:https://www.cnblogs.com/diorvh/p/12020122.html
Copyright © 2020-2023  润新知