zoukankan      html  css  js  c++  java
  • 【日记】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分治,多巩固一下之前的字符串和数学,就先这样吧。
  • 相关阅读:
    HBase with MapReduce (MultiTable Read)
    HBase with MapReduce (SummaryToFile)
    HBase with MapReduce (Summary)
    HBase with MapReduce (Read and Write)
    HBase with MapReduce (Only Read)
    Hbase中的BloomFilter(布隆过滤器)
    HBase的快照技术
    How To Use Hbase Bulk Loading
    Cloudera-Manager修改集群的IP
    Java中的HashSet和TreeSet
  • 原文地址:https://www.cnblogs.com/diorvh/p/12020122.html
Copyright © 2011-2022 走看看