【日记】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
using namespace std;
#define mid ((l+r)>>1)
#define db(x) cout<<#x<<":"< lsh;
unordered_map rev;
struct Opt{
    char op[2];
    int l,r,k;
    Opt():l(0),r(0),k(0){
        op[0]='\000';
    }
}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

总结

今天一天实验室,实验做的并不好,大家都不是很高兴。当然也没有什么时间写题,只做了这一个。但是写的过程中真的收获了很多,写的第二道树套树。关键还是要想明白,想明白,写代码也不会出问题,就更不需要调试了。

那么香港H题也就会了,区间查询的平衡树(二逼平衡树)也就可以A穿了。所以这么看,香港打铜尾是说明我自己真的菜。

明日计划

  1. 二逼平衡树P3380
  2. 香港H题,过不了的话就学一下离线做法。
  3. FHQ-treap和替罪羊树就先不学了吧,感觉学了也练不好,就最后学一下万能的CDQ分治,多巩固一下之前的字符串和数学,就先这样吧。

你可能感兴趣的:(【日记】12.10)