三个半小时考试,三个小时调试-----巨容易写炸的nlogn数据结构专题

1.树状数组

         这个是最最最简单的nlogn的数据结构啦,非常好写也不容易写错(就算写错也比较容易检查)

         相比其他bt的数据结构,树状数组的代码非常简洁,就是c数组有点抽象,晦涩难懂

         主要用来单点修改,区间查询。也可以通过差分来进行区间修改和区间查询,但一般区间修改一般不止加加减减这么简单啦,所以区间修改还是线段树会比较常用。

         总体还是很easy的,不多bb直接粘代码

    

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <queue>
#define maxn 600000
#define inf 0x7fffff
using namespace std;

int n,m,x,y,k,op;
int a[maxn],c[maxn];

int read()
{
    int xx=0,kk=1;char p=' ';
    while(!isdigit(p)){p=getchar();if(p=='-')kk=-1;}
    while(isdigit(p)){xx=xx*10+p-'0';p=getchar();}
    return kk*xx;
}

int lowbit(int x)
{
    return x&-x;
}

void add(int x,int k)
{
    while(x<=n)
    {
        c[x]+=k;
        x+=lowbit(x);
    }
}

int query(int x)
{
    int ans=0;
    while(x)
    {
        ans+=c[x];
        x-=lowbit(x);
    }
    return ans;
}

int main()
{
    n=read();m=read();
    for(int i=1;i<=n;++i) a[i]=read(),add(i,a[i]);    //注意一开始给c数组赋值
    for(int j=1;j<=m;++j)
    {
        op=read();x=read();
        switch(op)
        {
            case 1:k=read();add(x,k);break;
            case 2:y=read();printf("%lld\n",query(y)-query(x-1));break;
        }
    }
    return 0;
}

 

2.线段树

         又臭又长的数据结构的开端,一开始觉得线段树好长啊,好难写啊,好烦啊怎么这么多函数啊,啊我怎么又写炸啦。现在回过头来看线段树,真的是非常良心的一个数据结构啦!

        线段树的用途很多,当然很多都是想不到的(现在出题人真的丧心病狂,什么神学操作都有= =)

        相比起treap和splay线段树真的是太好写了(暴风哭泣),比起树状数组又好理解并且实用性广一些

        也是直接上代码啦~

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <queue>
#define maxn 800000+10
#define inf 0x7fffff
using namespace std;

int n,m,l,r,op;
int ll[maxn],rr[maxn];
long long k,a[maxn],w[maxn],tag[maxn];

long long read()
{
    long long xx=0,kk=1;char p=' ';
    while(!isdigit(p)){p=getchar();if(p=='-')kk=-1;}
    while(isdigit(p)){xx=xx*10+p-'0';p=getchar();}
    return kk*xx;
}

void pushdown(int num)
{
    tag[num<<1]+=tag[num];
    tag[num<<1|1]+=tag[num];
    w[num<<1]+=tag[num]*(rr[num<<1]-ll[num<<1]+1);//打tag的时候顺便修改w,虽然不知道为什么,但这样做貌似在递归时会方便一点点
    w[num<<1|1]+=tag[num]*(rr[num<<1|1]-ll[num<<1|1]+1);
    tag[num]=0;
}

void build(int l,int r,int num)
{
    ll[num]=l,rr[num]=r;
    if(l==r){w[num]=a[l];return;}
    int mid=(l+r)>>1;
    build(l,mid,num<<1);
    build(mid+1,r,num<<1|1);
    w[num]=w[num<<1]+w[num<<1|1];
}

void add(int l,int r,int k,int num)
{
    if(ll[num]>r||rr[num]<l)return;
    if(ll[num]>=l&&rr[num]<=r)
    {
        tag[num]+=k;
        w[num]+=k*(rr[num]-ll[num]+1);
        return;
    }
    pushdown(num);//注意修改时要pushdown标记,因为要靠子节点的w来修改当前节点的w
    add(l,r,k,num<<1);
    add(l,r,k,num<<1|1);
    w[num]=w[num<<1]+w[num<<1|1];
}

long long query(int l,int r,int num)
{
    if(ll[num]>r||rr[num]<l)return 0;
    if(ll[num]>=l&&rr[num]<=r)return w[num];
    pushdown(num);
    return query(l,r,num<<1)+query(l,r,num<<1|1);
}

int main()
{
    n=read();m=read();
    for(int i=1;i<=n;++i)a[i]=read();
    build(1,n,1);
    for(int j=1;j<=m;++j)
    {
        op=read();l=read();r=read();
        switch(op)
        {
            case 1:{k=read();add(l,r,k,1);break;}
            case 2:{printf("%lld\n",query(l,r,1));break;}
        }
    }
    return 0;
}

 


3.权值线段树

         听说是个黑科技,貌似能代替treap和splay的弱者福音??(但蒟蒻我还是打不来

4.带旋treap

         虽说是省选内容但是现在noip基本都有涉及,为了达成万水水的要求当然还是要熟练掌握。

         一开始是照着石学姐的代码码的,懵逼中把模板题水了过去。现在回过头来好好理解了一下代码,加入了一些自己的理解(还是自己码的代码看起来舒服qvq)打算能像xio姐一样十分钟一个板子

          自己的理解都在注释中啦,感觉还是比较清晰吧qvq

#include <iostream>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#define ll long long
#define maxn 200000+5
#define inf 0x7fffffff
using namespace std;

int opt,n,x,tot,root;
int son[maxn][2],rnd[maxn],cnt[maxn],val[maxn],sz[maxn];

int read()
{
    int xx=0,kk=1;char p=' ';
    while(!isdigit(p)){p=getchar();if(p=='-')kk=-1;}
    while(isdigit(p)){xx=xx*10+p-'0';p=getchar();}
    return xx*kk;
}

void update(int pos)
{
    sz[pos]=sz[son[pos][0]]+sz[son[pos][1]]+cnt[pos];//sz是该节点及其下面子节点的数的个数之和,cnt是等于当前节点的值的数的个数 
}

void rotate(int &pos,int p)//p为1时左旋(将右儿子转到父节点),p为0时右旋(将左儿子转到父节点) 
{
    int s=son[pos][p];
    sz[s]=sz[pos];
    son[pos][p]=son[s][p^1];
    son[s][p^1]=pos;
    update(pos);
    pos=s;
} 

void insert(int &pos,int x)
{
    if(!pos)
    {
        pos=++tot;
        val[pos]=x;//val使treap维持二叉树的性质(权值 右>中>左 
        rnd[pos]=rand();//rnd使treap维持堆的性质(使树深度尽可能变成logn 
    }
    sz[pos]++;
    if(val[pos]==x){cnt[pos]++;return;}
    int p=x>val[pos];
    insert(son[pos][p],x);
    if(rnd[son[pos][p]]<rnd[pos]) rotate(pos,p);
}

void del(int &pos,int x)
{
    if(!pos)return;
    if(val[pos]==x)
    {
        if(cnt[pos]>1){sz[pos]--;cnt[pos]--;return;}
        if(!(sz[son[pos][0]]*sz[son[pos][1]])){pos=son[pos][0]+son[pos][1];return;}
        int p=rnd[son[pos][0]]>rnd[son[pos][1]];//将左右儿子中优先级小的转到父节点 
        rotate(pos,p);sz[pos]--;
        del(son[pos][p^1],x);
    }
    else{sz[pos]--;del(son[pos][x>val[pos]],x);}
}

int qrank(int pos,int x)
{
    if(val[pos]==x)return sz[son[pos][0]]+1;
    if(x<val[pos])return qrank(son[pos][0],x);
    return sz[son[pos][0]]+cnt[pos]+qrank(son[pos][1],x);
}

int qnum (int pos,int x)
{
    if(x>sz[son[pos][0]]&&x<=sz[son[pos][0]]+cnt[pos])return val[pos];//也可以写成x<=sz[pos]-sz[son[pos][1]]; 
    if(x<=sz[son[pos][0]])return qnum(son[pos][0],x);
    return qnum(son[pos][1],x-sz[son[pos][0]]-cnt[pos]); 
}

int qpre(int pos,int x)
{
    if(!pos)return -inf;
    if(x>val[pos])return max(val[pos],qpre(son[pos][1],x));//将符合不大于x的数都进行比较,最大值就是前驱前驱 
    else return qpre(son[pos][0],x); 
}

int qnxt(int pos,int x)
{
    if(!pos)return inf;
    if(x<val[pos])return min(val[pos],qnxt(son[pos][0],x));//同上 
    else return qnxt(son[pos][1],x);
}

int main()
{
    srand(time(NULL));
    n=read();
    for(int i=1;i<=n;++i)
    {
        opt=read();x=read();
        switch(opt)
        {
            case 1:insert(root,x);break;
            case 2:del(root,x);break;
            case 3:printf("%d\n",qrank(root,x));break;
            case 4:printf("%d\n",qnum(root,x));break;
            case 5:printf("%d\n",qpre(root,x));break;
            case 6:printf("%d\n",qnxt(root,x));break;
        }
    }
    return 0; 
}

 

5.无旋treap

         貌似和带旋的差不多,为了巩固treap应该还是会找个时间学学的qwq

6.splay

         splay更是懵中带懵,跟着大佬的代码码了几遍,有点点自己的理解了,但要自己实现目前还有点点困难qwq

         要注意的是一颗splay很难同时实现翻转的插入,删除操作的。因为进行翻转操作时是打的tag并没有真正的翻转,只有在输出的时候才下放标记,下放标记的同时又破坏了二叉树的性质。但听机房大佬说貌似可以存个时间点,然后进行一些骚操作(太强啦!),反正我是不会啦!

        splay中很多小细节很容易写炸,需要多加练习

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <ctime>
#define maxn 100000+10
using namespace std;

int n,m,l,r,root,tot;
int son[maxn][2],sz[maxn],fa[maxn],w[maxn],mark[maxn];

int read()
{
    int xx=0,kk=1;char p=' ';
    while(!isdigit(p)){p=getchar();if(p=='-') kk=-1;}
    while(isdigit(p)){xx=xx*10+p-'0';p=getchar();}
    return kk*xx;
}

void init(int n,int x,int f)
{
    sz[n]=1,w[n]=x,fa[n]=f;
}

void pushup(int x)
{
    sz[x]=sz[son[x][0]]+sz[son[x][1]]+1;//由于模板题中没有重复元素,所以直接+1就ok了,如果重复的话此处把1改为cnt[x] 
}

void pushdown(int x)
{
    if(!mark[x])return;
    mark[son[x][0]]^=1;
    mark[son[x][1]]^=1;
    mark[x]=0;
    swap(son[x][0],son[x][1]);//交换左右儿子的下标 
}

void rotate(int x)//注意赋值顺序!! 
{
    int pre=fa[x],ppre=fa[pre];
    int pos=son[pre][1]==x;
    int ppos=son[ppre][1]==pre;
    son[ppre][ppos]=x;fa[x]=ppre;
    son[pre][pos]=son[x][pos^1];
    fa[son[x][pos^1]]=pre;
    son[x][pos^1]=pre;fa[pre]=x;//这句话应该放在最后面,因为会影响前面的赋值操作 
    pushup(pre);pushup(x);
}

void splay(int x,int goal)
{
    while(fa[x]!=goal)
    {
        int pre=fa[x],ppre=fa[pre];
        if(ppre!=goal) 
            son[ppre][1]==pre^son[pre][1]==x?rotate(x):rotate(pre);//该节点机器父节点都为左节点或右节点时先转父节点再转该节点,不知道为什么==,可能是为了保持平衡吧ww 
        rotate(x);
    }
    if(!goal) root=x;
}

void insert(int x)
{
    int u=root,pre=0;
    while(u) pre=u,u=son[u][x>w[u]];
    u=++tot;
    if(pre)son[pre][x>w[pre]]=u;
    init(u,x,pre);
    splay(u,0);
}

int rank(int x)
{
    int u=root;
    while(1)
    {
        pushdown(u);
        if(sz[son[u][0]]>=x)u=son[u][0];
        else if(sz[son[u][0]]+1==x)return u;
        else x-=sz[son[u][0]]+1,u=son[u][1];
    }
}

void work(int l,int r)
{
    l=rank(l);r=rank(r+2);
    splay(l,0);splay(r,l);
    mark[son[son[root][1]][0]]^=1;//在大于l小于r+2的节点上打标记,最后输出结果时要-1 
}

void print(int u)
{
    pushdown(u);
    if(son[u][0]) print(son[u][0]);
    if(w[u]>1&&w[u]<n+2)printf("%d ",w[u]-1);
    if(son[u][1]) print(son[u][1]);
}

int main()
{
    n=read();m=read();
    for(int i=1;i<=n+2;++i)insert(i);
    while(m--)
    {
        l=read();r=read();
        work(l,r);
    }
    print(root);
    return 0;    
}

 

7.主席树

           主席树貌似是提高组考点来着orz...然而弱爆了的南瓜现在才会打板子呜呜呜

           目前了解到的主席树的作用一共就两个吧(学艺不精QAQ)

        1.区间k大值

                     考虑离散化,按权值从小到大进行插入操作,然后对于一个查询r到l的k大值,我们只需要将第r颗树减去第l颗树,就可以得到在原来每个区间中有多少个数,在树上跑一遍第k个数的位置,再把值输出来就好啦(主要是有一个离散化后的数组和原来的数组说起来会有点点绕orz,由于南瓜语言表达能力实在是太弱啦,就感性理解一下下吧qwq

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <ctime>
#define maxn 200010<<5
#define debug(x) cout<<#x<<" = "<<x<<endl;
using namespace std;

int n,m,end,cnt,pos,l,r,k;
int a[maxn],b[maxn],root[maxn],sum[maxn],son[maxn][2];

int read()
{
    int xx=0,kk=1;char ch=' ';
    while(!isdigit(ch)){ch=getchar();if(ch=='-')kk=-1;}
    while(isdigit(ch)){xx=xx*10+ch-'0';ch=getchar();}
    return kk*xx;
}

void build(int &num,int l,int r)
{
    num=++cnt;
    if(l==r)return;
    int mid=(l+r)>>1;
    build(son[num][0],l,mid);
    build(son[num][1],mid+1,r);
}

int add(int pre,int l,int r)
{
    int now=++cnt;sum[now]=sum[pre]+1;
    son[now][0]=son[pre][0],son[now][1]=son[pre][1];
    if(l==r) return now;
    int mid=(l+r)>>1;
    if(pos<=mid) son[now][0]=add(son[now][0],l,mid);
    else son[now][1]=add(son[now][1],mid+1,r);
    return now;
}

int query(int x,int y,int l,int r,int k)
{
    if(l==r) return b[l];
    int mid=(l+r)>>1,del=sum[son[x][0]]-sum[son[y][0]];
    if(del>=k) return query(son[x][0],son[y][0],l,mid,k);
    return query(son[x][1],son[y][1],mid+1,r,k-del);
}

int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;++i)
        b[i]=a[i]=read();
    sort(b+1,b+1+n);
    end=unique(b+1,b+1+n)-b-1;
    build(root[0],1,end);
    for(int i=1;i<=n;++i)
    {
        pos=lower_bound(b+1,b+end+1,a[i])-b;
        root[i]=add(root[i-1],1,end);
    }
    for(int i=1;i<=m;++i)
    {
        l=read(),r=read(),k=read();
        printf("%d\n",query(root[r],root[l-1],1,end,k));
    }
    return 0;
}

 

        2.求t时刻的值

                     这个很好理解啦,比没有前一个那么绕orz,就直接上代码啦

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <ctime>
#define maxn 1000005<<5
#define debug(x) cout<<#x<<" = "<<x<<endl;
using namespace std;

int n,m,val,cnt,tim,opt,pos;
int a[maxn],root[maxn],w[maxn],son[maxn][2];

int read()
{
    int xx=0,kk=1;char ch=' ';
    while(!isdigit(ch)){ch=getchar();if(ch=='-')kk=-1;}
    while(isdigit(ch)){xx=xx*10+ch-'0';ch=getchar();}
    return kk*xx;
}

void build(int &now,int l,int r)
{    
    now=++cnt;
    if(l==r){w[now]=a[l];return;} 
    int mid=(l+r)>>1;
    build(son[now][0],l,mid);
    build(son[now][1],mid+1,r);
}

void add(int &now,int pre,int l,int r,int pos,int val)
{
    
    now=++cnt;w[now]=w[pre];
    son[now][0]=son[pre][0],son[now][1]=son[pre][1];
    if(l==r){w[now]=val;return;}
    int mid=(l+r)>>1;
    if(mid>=pos) add(son[now][0],son[pre][0],l,mid,pos,val);
    else add(son[now][1],son[pre][1],mid+1,r,pos,val);
}

int query(int now,int l,int r,int pos)
{
    if(l==r) return w[now];
    int mid=(l+r)>>1;
    if(mid>=pos) return query(son[now][0],l,mid,pos);
    else return query(son[now][1],mid+1,r,pos);
}

int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;++i) a[i]=read();
    build(root[0],1,n);
    for(int i=1;i<=m;++i) 
    {
        tim=read(),opt=read(),pos=read();
        switch(opt)
        {
            case 1:{val=read();add(root[i],root[tim],1,n,pos,val);break;}
            case 2:{printf("%d\n",query(root[tim],1,n,pos));root[i]=root[tim];break;}
        }
    }
    return 0; 
}

 

        然后后排提示一下,主席树的空间非常炸,一般开到nlogn(一般是开2^5倍叭?!)就收手啦,小了会re,大了会mle,不仅如此,大常数选手南瓜还疯狂在tle的边缘试探qwq

                  

8.LCT

 

最后这个由于我太菜啦,所以连干什么的都不知道QAQ

 

果然还是太弱啦!还要加油哦qvq

 

To Be Continued...

posted @ 2018-07-17 21:26  ฅ南瓜ฅ  阅读(213)  评论(0编辑  收藏  举报