Loading

【学习笔记】高级数据结构

Page Views Count

平衡树

平衡树一般操作是插入、删除、查排名、查排名对应权值以及查前驱后继。

Treap

算法思想

Treap 是二叉搜索树与堆的结合,给每个节点一个随机附加权值 \(pri\),根据原权值维护二叉搜索树,根据随机附加权值维护堆。

也就是保证,对于每个节点,其左子树内原权值都小于该节点原权值,右子树内原权值都大于该节点原权值,而其子树内所有节点的附加权值都大于该节点附加权值。

也就是当随机的附加权值不满足堆的性质时,我们需要调整树的形态以保证 \(\log\) 的期望树高。

具体操作

  • 左旋右旋:Treap 的核心,对于节点 \(x\),其左右儿子 \(y,z\),右旋即,将 \(y\) 作为根,而 \(x\) 成为 \(y\) 的右儿子,对原权值而言,\(y\) 原来的右儿子 \(c\) 满足:\(y<c<x<z\),因此 \(c\) 就应当放在 \(x\) 的左儿子处。左旋同理。

  • 插入:按照原权值在树上找到对应位置,如果没有相等位置则新开一个节点,根据附加权值调整旋转。

  • 删除:找到对应位置,讨论节点是否会删去,若会删去则讨论儿子个数,单个儿子直接提上来,两个儿子将当前节点转下去。

  • 其余查询操作:平衡树上二分。

模板

点击查看代码
struct Treap{
    int rt,tot;
    int ch[maxn][2],val[maxn],pri[maxn],cnt[maxn],siz[maxn];
    inline void push_up(int x){
        siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];
    }
    inline int new_node(int k){
        ++tot;
        val[tot]=k,pri[tot]=rand();
        cnt[tot]=1,siz[tot]=1;
        return tot;
    }
    inline void build(){
        new_node(-inf),new_node(inf);
        rt=1,ch[1][1]=2;
        push_up(rt);
    }
    inline void left_rotate(int &x){
        int y=ch[x][1];
        ch[x][1]=ch[y][0],ch[y][0]=x;
        push_up(x),push_up(y);
        x=y;
    }
    inline void right_rotate(int &x){
        int y=ch[x][0];
        ch[x][0]=ch[y][1],ch[y][1]=x;
        push_up(x),push_up(y);
        x=y;
    }
    void insert(int &x,int k){
        if(!x) return x=new_node(k),void();
        if(k==val[x]) ++cnt[x];
        else if(k<val[x]){
            insert(ch[x][0],k);
            if(pri[ch[x][0]]<pri[x]) right_rotate(x);
        }
        else{
            insert(ch[x][1],k);
            if(pri[ch[x][1]]<pri[x]) left_rotate(x);
        }
        push_up(x);
    }
    void erase(int &x,int k){
        if(!x) return;
        if(k==val[x]){
            if(cnt[x]>1) --cnt[x];
            else if(!ch[x][0]||!ch[x][1]) x=ch[x][0]+ch[x][1];
            else if(pri[ch[x][0]]<pri[ch[x][1]]){
                right_rotate(x);
                erase(ch[x][1],k);
            }
            else{
                left_rotate(x);
                erase(ch[x][0],k);
            }
        }
        else if(k<val[x]) erase(ch[x][0],k);
        else erase(ch[x][1],k);
        push_up(x);
    }
    int rk(int x,int k){
        if(!x) return 0;
        if(k==val[x]) return siz[ch[x][0]]+1;
        else if(k<val[x]) return rk(ch[x][0],k);
        else return siz[ch[x][0]]+cnt[x]+rk(ch[x][1],k);
    }
    int kth(int x,int k){
        if(!x) return 0;
        if(siz[ch[x][0]]>=k) return kth(ch[x][0],k);
        else if(siz[ch[x][0]]+cnt[x]>=k) return val[x];
        else return kth(ch[x][1],k-(siz[ch[x][0]]+cnt[x]));
    }
    inline int get_pre(int k){
        int x=rt,res=-inf;
        while(x){
            if(k==val[x]){
                if(ch[x][0]){
                    x=ch[x][0];
                    while(ch[x][1]) x=ch[x][1];
                    res=val[x];
                }
                break;
            }
            if(k<val[x]) x=ch[x][0];
            else{
                res=max(res,val[x]);
                x=ch[x][1];
            }
        }
        return res;
    }
    inline int get_nxt(int k){
        int x=rt,res=inf;
        while(x){
            if(k==val[x]){
                if(ch[x][1]){
                    x=ch[x][1];
                    while(ch[x][0]) x=ch[x][0];
                    res=val[x];
                }
                break;
            }
            if(k<val[x]){
                res=min(res,val[x]);
                x=ch[x][0];
            }
            else x=ch[x][1];
        }
        return res;
    }
}T;

fhq-Treap

算法思想

无旋 Treap,通过分裂合并来维持平衡。

合并就是使其满足堆的性质,分裂一般按照一个阈值 \(k\),分成原权值小于等于和大于两部分。

具体操作

  • 合并:当前根当中选择一个附加权值小的作为根,剩下的向下合并。

  • 分裂:根据阈值向下二分,将整个子树划入某个分裂树中。

  • 插入:按照权值分裂,先与新节点合并再整体合并。

  • 删除:按照权值分裂,再按照权值 \(-1\) 分裂,这样得到的中间子树的根就是要删除的节点。

  • 查询排名:按照权值分裂,左子树大小 \(+1\) 即为答案。

  • 查询排名对应权值:平衡树上二分。

  • 查询前驱后继:按照权值分裂,再查询子树中的最小或最大值。

模板

点击查看代码
struct fhq_Treap{
    int rt,tot;
    int ch[maxn][2],val[maxn],pri[maxn],siz[maxn];
    inline void push_up(int x){
        siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
    }
    inline int new_node(int k){
        ++tot;
        val[tot]=k,pri[tot]=rand(),siz[tot]=1;
        return tot;
    }
    inline void build(){
        new_node(-inf),new_node(inf);
        rt=1,ch[1][1]=2;
        push_up(rt);
    }
    int merge(int x,int y){
        if(!x||!y) return x+y;
        if(pri[x]<pri[y]){
            ch[x][1]=merge(ch[x][1],y);
            push_up(x);
            return x;
        }
        else{
            ch[y][0]=merge(x,ch[y][0]);
            push_up(y);
            return y;
        }
    }
    void split(int p,int k,int &x,int &y){
        if(!p) x=0,y=0;
        else{    
            if(val[p]<=k) x=p,split(ch[p][1],k,ch[x][1],y);
            else y=p,split(ch[p][0],k,x,ch[y][0]);
            push_up(p); 
        }
    }
    inline void insert(int k){
        int x,y;
        split(rt,k,x,y);
        rt=merge(merge(x,new_node(k)),y);
    }
    inline void erase(int k){
        int x,y,z;
        split(rt,k,x,z),split(x,k-1,x,y);
        y=merge(ch[y][0],ch[y][1]);
        rt=merge(merge(x,y),z);
    }
    inline int rk(int k){
        int x,y;
        split(rt,k-1,x,y);
        int res=siz[x]+1;
        rt=merge(x,y);
        return res;
    }
    int kth(int x,int k){
        if(!x) return 0;
        if(siz[ch[x][0]]>=k) return kth(ch[x][0],k);
        else if(siz[ch[x][0]]+1>=k) return val[x];
        else return kth(ch[x][1],k-(siz[ch[x][0]]+1));
    }
    inline int get_pre(int k){
        int x,y;
        split(rt,k-1,x,y);
        int res=kth(x,siz[x]);
        rt=merge(x,y);
        return res;
    }
    inline int get_nxt(int k){
        int x,y;
        split(rt,k,x,y);
        int res=kth(y,1);
        rt=merge(x,y);
        return res;
    }
}T;

Splay

算法思想

使用一种高级操作维护平衡,每次伸展使得一个节点旋转到根从而维持平衡。

由于这种旋转到指定位置的特性,Splay 对序列操作非常友好。

具体操作

  • 单次旋转:与 Treap 的左旋右旋相同,只不过写在了一起。

  • 多次旋转:要考虑当前节点与父节点的儿子身份是否相同,即是否是同向儿子,如果是先旋转父亲再旋转儿子即可,反之在旋转父亲时,儿子会被转到另一个节点的子节点,因此要连续两次旋转当前节点。

  • 插入:找到对应位置直接插入。

  • 删除操作,找到前驱并旋转到根,找到后继并旋转到前驱的儿子,这样前驱右子树都是大于这个权值的,而右子树的根的后继,也就是说右子树的左儿子就是这个数。

  • 找到一个权值所在位置并旋转到根:平衡树上二分。

  • 查询排名:找到前驱后继

  • 查询排名对应权值:平衡树上二分。

  • 查询前驱后继:找到所在位置并旋转到根,再向下找到对应答案。

模板

点击查看代码
struct Splay{
    int rt,tot;
    int fa[maxn],ch[maxn][2];
    int val[maxn],cnt[maxn],siz[maxn];
    inline void push_up(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
    inline bool chkson(int x){return x==ch[fa[x]][1];}
    inline void rotate(int x){
        int y=fa[x],z=fa[y];
        bool chk=chkson(x);
        if(z) ch[z][chkson(y)]=x;
        fa[x]=z;
        ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
        ch[x][chk^1]=y,fa[y]=x;
        push_up(y),push_up(x);
    }
    inline void splay(int x,int goal=0){
        while(fa[x]!=goal){
            int y=fa[x],z=fa[y];
            if(z!=goal) rotate(chkson(x)==chkson(y)?y:x);
            rotate(x);
        }
        if(!goal) rt=x;
    }
    inline void insert(int k){
        int x=rt,f=0;
        while(x&&val[x]!=k) f=x,x=ch[x][val[x]<k];
        if(x) ++cnt[x];
        else{
            x=++tot;
            fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=1,siz[x]=1;
            if(f) ch[f][val[f]<k]=x;
        }
        splay(x);
    }
    inline void find(int k){
        int x=rt;
        while(ch[x][val[x]<k]&&val[x]!=k) x=ch[x][val[x]<k];
        splay(x);
    }
    inline int pre_nxt(int k,bool type){
        find(k);
        int x=rt;
        if(val[x]<k&&!type) return x;
        if(val[x]>k&&type) return x;
        x=ch[x][type];
        while(ch[x][type^1]) x=ch[x][type^1];
        return x;
    }
    inline int rank(int k){
        int x=pre_nxt(k,0);
        splay(x);
        return siz[ch[x][0]]+cnt[x]+1;
    }
    inline int kth(int k){
        int x=rt;
        while(1){
            if(k<=siz[ch[x][0]]) x=ch[x][0];
            else if(k<=siz[ch[x][0]]+cnt[x]) return val[x];
            else k-=siz[ch[x][0]]+cnt[x],x=ch[x][1];
        }
    }
    inline void erase(int k){
        int pre=pre_nxt(k,0),nxt=pre_nxt(k,1);
        splay(pre),splay(nxt,pre);
        int x=ch[nxt][0];
        if(cnt[x]>1){
            --cnt[x];
            splay(x);
        }
        else fa[x]=0,val[x]=0,siz[x]=0,cnt[x]=0,ch[nxt][0]=0;
    }
}S;

维护序列

平衡树不止可以维护单调序列,也可以维护一个正常的序列。实际上在正常 Splay 中只有插入时才会考虑到单调的问题,而 Splay 的伸展会保证中序遍历不变。也就是说如果我们将插入位置改变,不会影响到 Splay 的维护。于是在维护一个序列时,可以直接将 \(k\) 的位置检索与 \(siz\) 相关,就能做到动态插入删除元素,甚至是区间翻转等等。

找到一个元素或区间的前驱后继,再将这个元素或区间孤立,是处理区间的关键操作。

点击查看代码
struct Splay{
    int rt,tot;
    int fa[maxn],ch[maxn][2];
    int val[maxn],siz[maxn];
    bool tag[maxn];
    inline void push_up(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;}
    inline void push_down(int x){
        if(tag[x]){
            if(ch[x][0]) swap(ch[ch[x][0]][0],ch[ch[x][0]][1]),tag[ch[x][0]]^=1;
            if(ch[x][1]) swap(ch[ch[x][1]][0],ch[ch[x][1]][1]),tag[ch[x][1]]^=1;
            tag[x]=0;
        }
    }
    inline bool chkson(int x){return x==ch[fa[x]][1];}
    inline void build(){
        rt=2,tot=2;
        fa[1]=2,val[1]=0,siz[1]=1;
        ch[2][0]=1,val[2]=0,siz[2]=2;
    }
    inline void rotate(int x){
        int y=fa[x],z=fa[y];
        push_down(z),push_down(y),push_down(x);
        bool chk=chkson(x);
        if(z) ch[z][chkson(y)]=x;
        fa[x]=z;
        ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
        ch[x][chk^1]=y,fa[y]=x;
        push_up(y),push_up(x);
    }
    inline void splay(int x,int goal=0){
        while(fa[x]!=goal){
            int y=fa[x],z=fa[y];
            if(z!=goal) rotate(chkson(x)==chkson(y)?y:x);
            rotate(x);
        }
        if(!goal) rt=x;
    }
    inline int kth(int k){
        int x=rt;
        while(1){
            push_down(x);
            if(k<=siz[ch[x][0]]) x=ch[x][0];
            else if(k<=siz[ch[x][0]]+1) return x;
            else k-=siz[ch[x][0]]+1,x=ch[x][1];
        }
    }
    inline void insert(int x,int k){
        int pre=kth(x-1),nxt=kth(x);
        splay(pre),splay(nxt,pre);
        ++tot;
        fa[tot]=nxt,val[tot]=k,siz[tot]=1;
        ch[nxt][0]=tot;
        splay(tot);
    }
    inline void reverse(int l,int r){
        int pre=kth(l-1),nxt=kth(r+1);
        splay(pre),splay(nxt,pre);
        int x=ch[nxt][0];
        swap(ch[x][0],ch[x][1]),tag[x]^=1;
        splay(x);
    }
    inline void output(int x){
        push_down(x);
        if(ch[x][0]) output(ch[x][0]);
        if(val[x]) printf("%d ",val[x]);
        if(ch[x][1]) output(ch[x][1]);
    }
}S;

可持久化

支持对历史版本访问修改的一类数据结构。

可持久化线段树

算法思想

分可持久化线段树与可持久化权值线段树(主席树)两种,主要思想差不多。

考虑对每个历史版本单独建树,空间复杂度 \(O(n^2)\),而实际上每次单点修改只会修改 \(\log\) 个点,区间修改只会修改 \(\log^2\) 的个点,可以考虑动态开点,没有被修改的点直接共用,就起到了优化空间复杂度的效果。

对于单点的复杂度是 \(O(n\log n)\),区间的复杂度是 \(O(n\log^2 n)\)

这样操作的唯一问题是:多个历史版本的节点共用同一儿子会使得无法进行区间懒标记下传,于是使用标记永久化,即每次不下传标记并维护子树内信息,每次查询只查询经过的节点以及恰好覆盖节点子树内的贡献。

模板

可持久化线段树

点击查看代码
struct SegmentTree{
    #define mid ((l+r)>>1)
    int tot;
    int ch[maxm][2],val[maxm];
    inline void build(int &pos,int l,int r){
        pos=++tot;
        if(l==r) return val[pos]=a[l],void();
        build(ch[pos][0],l,mid);
        build(ch[pos][1],mid+1,r);
    }
    inline void new_node(int &pos){
        ++tot;
        ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],val[tot]=val[pos];
        pos=tot;
    }
    void insert(int &pos,int l,int r,int p,int k){
        new_node(pos);
        if(l==r) return val[pos]=k,void();
        if(p<=mid) insert(ch[pos][0],l,mid,p,k);
        else insert(ch[pos][1],mid+1,r,p,k);
    }
    int query(int pos,int l,int r,int p){
        if(l==r) return val[pos];
        if(p<=mid) return query(ch[pos][0],l,mid,p);
        else return query(ch[pos][1],mid+1,r,p);
    }
    #undef mid
}S;

可持久化权值线段树(主席树)

点击查看代码
struct SegmentTree{
    #define mid ((l+r)>>1)
    int tot;
    int ch[maxm][2],siz[maxm];
    inline void new_node(int &pos){
        ++tot;
        ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],siz[tot]=siz[pos];
        pos=tot;
    }
    void insert(int &pos,int l,int r,int p){
        new_node(pos);
        ++siz[pos];
        if(l==r) return;
        if(p<=mid) insert(ch[pos][0],l,mid,p);
        else insert(ch[pos][1],mid+1,r,p);
    }
    int query(int lpos,int rpos,int l,int r,int k){
        if(l==r) return l;
        int lsiz=siz[ch[rpos][0]]-siz[ch[lpos][0]];
        if(k<=lsiz) return query(ch[lpos][0],ch[rpos][0],l,mid,k);
        else return query(ch[lpos][1],ch[rpos][1],mid+1,r,k-lsiz);
    }
    #undef mid
}S;

可持久化平衡树

算法思想

Treap 与 Splay 旋转对可持久化非常不友好。

可持久化使用的是无旋的 fhq-Treap。

树的形态发生改变是在合并分裂时产生的,于是在这两个操作中新建节点。

模板

点击查看代码
struct fhq_Treap{
    int tot;
    int ch[maxm][2],val[maxm],pri[maxm],siz[maxm];
    inline void push_up(int x){
        siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
    }
    inline int new_node(int k){
        ++tot;
        val[tot]=k,pri[tot]=rand(),siz[tot]=1;
        return tot;
    }
    inline int cpy_node(int &p){
        ++tot;
        ch[tot][0]=ch[p][0],ch[tot][1]=ch[p][1];
        val[tot]=val[p],pri[tot]=pri[p],siz[tot]=siz[p];
        return tot;
    }
    int merge(int x,int y){
        if(!x||!y) return x+y;
        if(pri[x]<pri[y]){
            int z=cpy_node(x);
            ch[z][1]=merge(ch[z][1],y);
            push_up(z);
            return z;
        }
        else{
            int z=cpy_node(y);
            ch[z][0]=merge(x,ch[z][0]);
            push_up(z);
            return z;
        }
    }
    void split(int p,int k,int &x,int &y){
        if(!p) x=0,y=0;
        else{
            if(val[p]<=k){
                x=cpy_node(p);
                split(ch[p][1],k,ch[x][1],y);
                push_up(x);
            }
            else{
                y=cpy_node(p);
                split(ch[p][0],k,x,ch[y][0]);
                push_up(y);
            }
        }
    }
    inline void insert(int &p,int k){
        int x,y;
        split(p,k,x,y);
        p=merge(merge(x,new_node(k)),y);
    }
    inline void erase(int &p,int k){
        int x,y,z;
        split(p,k,x,z),split(x,k-1,x,y);
        y=merge(ch[y][0],ch[y][1]);
        p=merge(merge(x,y),z);
    }
    inline int rk(int &p,int k){
        int x,y;
        split(p,k-1,x,y);
        int res=siz[x]+1;
        p=merge(x,y);
        return res;
    }
    int kth(int p,int k){
        if(!p) return 0;
        if(siz[ch[p][0]]>=k) return kth(ch[p][0],k);
        else if(siz[ch[p][0]]+1>=k) return val[p];
        else return kth(ch[p][1],k-(siz[ch[p][0]]+1));
    }
    inline int get_pre(int &p,int k){
        int x,y;
        split(p,k-1,x,y);
        int res=kth(x,siz[x]);
        p=merge(x,y);
        return res;
    }
    inline int get_nxt(int &p,int k){
        int x,y;
        split(p,k,x,y);
        int res=kth(y,1);
        p=merge(x,y);
        return res;
    }
}T;

可持久化字典树

算法思想

以 01-Trie 为主。

也是一个动态建树的做法,没有修改的节点共用,可以查询一个数在某个区间内最大异或和。

模板

点击查看代码
struct Trie{
    int tot;
    int ch[maxm][2],siz[maxm];
    inline void new_node(int &pos){
        ++tot;
        ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],siz[tot]=siz[pos];
        pos=tot;
    }
    void insert(int &pos,int k,int d){
        new_node(pos);
        ++siz[pos];
        if(!d) return;
        bool chk=k&(1<<d-1);
        insert(ch[pos][chk],k,d-1);
    }
    int query(int lpos,int rpos,int k,int d){
        if(!d) return 0;
        bool chk=k&(1<<d-1);
        if(siz[ch[rpos][!chk]]-siz[ch[lpos][!chk]]) return (1<<d-1)+query(ch[lpos][!chk],ch[rpos][!chk],k,d-1);
        else return query(ch[lpos][chk],ch[rpos][chk],k,d-1);
    }
}T;

树套树

可以处理比原有问题限制更多的问题,例如将全局改为区间,静态改为动态。

线段树套平衡树(二逼平衡树)

在原操作的基础上加上了区间的限制。

算法思想

考虑把线段树上每个区间开一棵平衡树,维护当前区间内所有信息。

修改操作实际是删除以及插入,在对应位置所在的每个区间都进行这样操作即可。

查询排名、前驱后继都是可以合并的,查询排名即在每个区间的排名之和,前驱即每个区间的前驱中最大的,后继即每个区间的后继中最小的。

以上四种操作在单棵平衡树中都是 \(O(\log n)\) 的,于是修改一次是 \(O(\log^2 n)\) 的。

查询排名为 \(k\) 的数,这一信息无法在多个区间中合并,二分答案即可,于是单次复杂度是 \(O(\log^2 n\log V)\)

模板

点击查看代码
int n,m;
int a[maxn];
struct Splay{
    int tot;
    int fa[maxm],ch[maxm][2],val[maxm],cnt[maxm],siz[maxm];
    inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
    inline bool pdson(int x){return x==ch[fa[x]][1];}
    inline int new_node(int k){
        ++tot;
        ch[tot][0]=ch[tot][1]=0,val[tot]=k,cnt[tot]=siz[tot]=1;
        return tot;
    }
    inline void rotate(int x){
        int y=fa[x],z=fa[y];
        bool chk=pdson(x);
        ch[z][pdson(y)]=x,fa[x]=z;
        ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
        ch[x][chk^1]=y,fa[y]=x;
        maintain(y),maintain(x);
    }
    inline void splay(int &rt,int x,int goal=0){
        while(fa[x]!=goal){
            int y=fa[x],z=fa[y];
            if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
            rotate(x);
        }
        if(!goal) rt=x;
    }
    inline void insert(int &rt,int k){
        int x=rt,y=0;
        while(x&&val[x]!=k) y=x,x=ch[x][k>val[x]];
        if(x) ++cnt[x];
        else{
            x=new_node(k);
            if(y) fa[x]=y,ch[y][k>val[y]]=x;
        }
        splay(rt,x);
    }
    inline void find(int &rt,int k){
        int x=rt;
        while(ch[x][k>val[x]]&&val[x]!=k) x=ch[x][k>val[x]];
        splay(rt,x);
    }
    inline int rk(int &rt,int k){
        find(rt,k);
        if(k>val[rt]) return siz[ch[rt][0]]+cnt[rt]-1;
        else return siz[ch[rt][0]]-1;
    }
    inline int kth(int rt,int k){
        int x=rt;
        while(1){
            if(siz[ch[x][0]]>=k) x=ch[x][0];
            else if(siz[ch[x][0]]+cnt[x]>=k) return val[x];
            else{
                k-=(siz[ch[x][0]]+cnt[x]);
                x=ch[x][1];
            }
        }
    }
    inline int pre_nxt(int &rt,int k,bool pd){
        find(rt,k);
        int x=rt;
        if(val[x]<k&&!pd) return x;
        if(val[x]>k&&pd) return x;
        x=ch[x][pd];
        while(ch[x][pd^1]) x=ch[x][pd^1];
        return x;
    }
    inline void erase(int &rt,int k){
        int pre=pre_nxt(rt,k,0),nxt=pre_nxt(rt,k,1);
        splay(rt,pre),splay(rt,nxt,pre);
        int x=ch[nxt][0];
        if(cnt[x]>1){
            --cnt[x];
            splay(rt,x);
        }
        else ch[nxt][0]=0;
    }
}T;

struct SegmentTree{
    #define mid ((l+r)>>1)
    #define lson pos<<1,l,mid
    #define rson pos<<1|1,mid+1,r
    int rt[maxn<<2];
    void build(int pos,int l,int r){
        T.insert(rt[pos],-inf),T.insert(rt[pos],inf);
        if(l==r) return;
        build(lson),build(rson);
    }
    inline void insert(int pos,int l,int r,int p){
        T.insert(rt[pos],a[p]);
        if(l==r) return;
        if(p<=mid) insert(lson,p);
        else insert(rson,p);
    }
    inline int rk(int pos,int l,int r,int pl,int pr,int k){
        if(pl<=l&&r<=pr) return T.rk(rt[pos],k);
        int res=0;
        if(pl<=mid) res+=rk(lson,pl,pr,k);
        if(pr>mid) res+=rk(rson,pl,pr,k);
        return res;
    }
    inline void update(int pos,int l,int r,int p,int k){
        T.erase(rt[pos],a[p]);
        T.insert(rt[pos],k);
        if(l==r) return; 
        if(p<=mid) update(lson,p,k);
        else update(rson,p,k);
    }
    inline int get_pre(int pos,int l,int r,int pl,int pr,int k){
        if(pl<=l&&r<=pr) return T.val[T.pre_nxt(rt[pos],k,0)];
        int res=-inf;
        if(pl<=mid) res=max(res,get_pre(lson,pl,pr,k));
        if(pr>mid) res=max(res,get_pre(rson,pl,pr,k));
        return res;
    }
    inline int get_nxt(int pos,int l,int r,int pl,int pr,int k){
        if(pl<=l&&r<=pr) return T.val[T.pre_nxt(rt[pos],k,1)];
        int res=inf;
        if(pl<=mid) res=min(res,get_nxt(lson,pl,pr,k));
        if(pr>mid) res=min(res,get_nxt(rson,pl,pr,k));
        return res; 
    }
    #undef mid
    #undef lson
    #undef rson
}S;

int main(){
    n=read(),m=read();
    S.build(1,1,n);
    for(int i=1;i<=n;++i){
        a[i]=read();
        S.insert(1,1,n,i);
    }
    while(m--){
        int opt=read();
        if(opt==1){
            int l=read(),r=read(),k=read();
            printf("%d\n",S.rk(1,1,n,l,r,k)+1);
        }
        else if(opt==2){
            int l=read(),r=read(),k=read();
            int L=-1e8,R=1e8,ans;
            while(L<=R){
                int mid=(L+R)>>1;
                if(S.rk(1,1,n,l,r,mid+1)+1>k) ans=mid,R=mid-1;
                else L=mid+1;
            }
            printf("%d\n",ans);
        }
        else if(opt==3){
            int p=read(),k=read();
            S.update(1,1,n,p,k);
            a[p]=k;
        }
        else if(opt==4){
            int l=read(),r=read(),k=read();
            printf("%d\n",S.get_pre(1,1,n,l,r,k));
        }
        else{
            int l=read(),r=read(),k=read();
            printf("%d\n",S.get_nxt(1,1,n,l,r,k));
        }
    }
    return 0;
}

树状数组套主席树

求动态区间 \(k\) 小值,用到主席树的前缀和差分思想。

算法思想

如果正常用暴力修改,那么修改复杂度是 \(O(n\log n)\) 而查询是 \(O(\log n)\) 的,二者不够平衡。

如果不维护前缀的权值线段树,而是对每个位置单独维护,修改是 \(O(\log n)\) 而查询变成 \(O(n\log n)\),也就是单点修改和区间求和,可以使用树状数组来平衡这一过程。

常规树状数组 \(x\) 维护的是 \([x-\mathrm{lowbit}(x)+1,x]\) 的和,这里就改成维护这个区间内的权值线段树,于是对于 \([1,x]\) 的权值线段树,查询和修改分别变成了对 \(O(\log n)\) 个区间对应的权值线段树操作,信息可以合并起来,于是复杂度就做到 \(O(n\log^2 n)\)

模板

点击查看代码
int n,m;
int a[maxn],rt[maxn];
struct Question{
    int l,r,x,k;
}q[maxn];
vector<int> V;
int lpos[maxn],rpos[maxn];
struct SegmentTree{
    #define mid ((l+r)>>1)
    int tot;
    int ch[maxm][2],siz[maxm];
    inline void new_node(int &pos){
        ++tot;
        ch[tot][0]=ch[tot][1]=0,siz[tot]=0;
        pos=tot;
    }
    void insert(int &pos,int l,int r,int p,int k){
        if(!pos) new_node(pos);
        siz[pos]+=k;
        if(l==r) return;
        if(p<=mid) insert(ch[pos][0],l,mid,p,k);
        else insert(ch[pos][1],mid+1,r,p,k);
    }
    int query(int l,int r,int k){
        if(l==r) return l;
        int lsiz=0;
        for(int i=1;i<=lpos[0];++i) lsiz-=siz[ch[lpos[i]][0]];
        for(int i=1;i<=rpos[0];++i) lsiz+=siz[ch[rpos[i]][0]];
        if(lsiz>=k){
            for(int i=1;i<=lpos[0];++i) lpos[i]=ch[lpos[i]][0];
            for(int i=1;i<=rpos[0];++i) rpos[i]=ch[rpos[i]][0];
            return query(l,mid,k);
        }
        else{
            for(int i=1;i<=lpos[0];++i) lpos[i]=ch[lpos[i]][1];
            for(int i=1;i<=rpos[0];++i) rpos[i]=ch[rpos[i]][1];
            return query(mid+1,r,k-lsiz);
        }
    }
    #undef mid
}S;
struct BinaryIndexedTree{
    #define lowbit(x) (x&-x)
    inline void update(int x,int k){
        int id=lower_bound(V.begin(),V.end(),a[x])-V.begin()+1;
        while(x<=n){
            S.insert(rt[x],1,V.size(),id,k);
            x+=lowbit(x);
        }
    }
    inline int query(int l,int r,int k){
        lpos[0]=rpos[0]=0;
        while(r){
            rpos[++rpos[0]]=rt[r];
            r-=lowbit(r);
        }
        --l;
        while(l){
            lpos[++lpos[0]]=rt[l];
            l-=lowbit(l);
        }
        return S.query(1,V.size(),k);
    }
    #undef lowbit
}B;
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;++i){
        a[i]=read();
        V.push_back(a[i]);
    }
    for(int i=1;i<=m;++i){
        char opt[4];
        scanf("%s",opt);
        if(opt[0]=='Q'){
            q[i].l=read(),q[i].r=read(),q[i].x=-1,q[i].k=read();
        }
        else{
            q[i].x=read(),q[i].k=read();
            V.push_back(q[i].k);
        }
    }
    sort(V.begin(),V.end());
    V.erase(unique(V.begin(),V.end()),V.end());
    for(int i=1;i<=n;++i) B.update(i,1);
    for(int i=1;i<=m;++i){
        if(q[i].x!=-1){
            B.update(q[i].x,-1);
            a[q[i].x]=q[i].k;
            B.update(q[i].x,1);
        }
        else printf("%d\n",V[B.query(q[i].l,q[i].r,q[i].k)-1]);
    }
    return 0;
}

参考资料

  • OI Wiki
posted @ 2022-12-21 19:58  SoyTony  阅读(165)  评论(3编辑  收藏  举报