平衡树

发布时间 2023-06-04 22:43:24作者: TKXZ133

FHQTreap

FHQTreap是一种平衡树,它通过分裂和合并两种操作来维持树的平衡,而不需要旋转。

其每个节点除了有权值,还有一个随机的优先级,用来决定合并的方案。单次操作的复杂度是\(O(\log n)\)

可以方便的实现很多区间功能,比如进行区间翻转、区间加法,维护区间最值、区间和等。

也可以很方便地实现可持久化,因为不需要修改父子关系。

其他应用

  • 动态凸包,可以支持点的插入和删除,以及查询凸包上的点。
  • 动态中位数,可以支持元素的插入和删除,以及查询中位数或任意顺序统计量。

FHQTreap通过随机权值保证单次操作期望复杂度为\(O(\log n)\)。这是因为在随机数均匀分布的情况下,其期望树高为$ 2\ln n\ \approx 1.39\log_2n\(,而单次合并分类操作是沿着树的高度进行递归的,所以每次分裂或合并的时间复杂度是\)O(\log n) \(的。而FHQTreap的其他操作均可以通过常数次合并分裂操作实现,故FHQTreap的总时间复杂度是\) O(\log n)$。但常数较大,通常有\(4\sim 5\)倍常数。

普通平衡树

普通平衡树

普通平衡树

即用平衡树对一个集合进行维护,支持插入,删除,求前驱,后继,查询某数的排名,查询排名对应的数等。

mt19937 rng(random_device{}());

struct FHQn{
    int ch[2];
    int val,key,siz;
};

struct FHQ{
    FHQn a[N];
    int tot,rt,x,y,z,ans;
    void push_up(int p){
        a[p].siz=a[a[p].ch[0]].siz+a[a[p].ch[1]].siz+1;
    }
    int built(int k){
        int p=++tot;
        a[p]={{0,0},k,rng(),1};
        return p;
    }
    int merge(int p,int q){
        if(!p||!q) return p+q;
        if(a[p].key<a[q].key){
            a[p].ch[1]=merge(a[p].ch[1],q);
            push_up(p);return p;
        }
        else{
            a[q].ch[0]=merge(p,a[q].ch[0]);
            push_up(q);return q;
        }
    }
    void split(int p,int k,int &l,int &r){
        if(!p){l=r=0;return ;}
        if(a[p].val<=k){
            l=p;split(a[p].ch[1],k,a[p].ch[1],r);
        }
        else{
            r=p;split(a[p].ch[0],k,l,a[p].ch[0]);
        }
        push_up(p);
    }
    int kth(int p,int k){
        while(true){
            if(a[a[p].ch[0]].siz>=k) p=a[p].ch[0];
            else if(a[a[p].ch[0]].siz+1==k) return p;
            else k-=a[a[p].ch[0]].siz+1,p=a[p].ch[1];
        }
    }
    void insert(int k){
        split(rt,k,x,y);
        rt=merge(merge(x,built(k)),y);
    }
    void del(int k){
        split(rt,k,x,z);split(x,k-1,x,y);
        y=merge(a[y].ch[0],a[y].ch[1]);
        rt=merge(merge(x,y),z);
    }
    int find_kth(int k){
        split(rt,k-1,x,y);
        ans=a[x].siz+1;
        rt=merge(x,y);
        return ans;
    }
    int find_num(int k){
        return a[kth(rt,k)].val;
    }
    int pre(int k){
        split(rt,k-1,x,y);
        ans=a[kth(x,a[x].siz)].val;
        rt=merge(x,y);
        return ans;
    }
    int nxt(int k){
        split(rt,k,x,y);
        ans=a[kth(y,1)].val;
        rt=merge(x,y);
        return ans;
    }
}tree;

序列平衡树

即用平衡树对序列进行维护,支持区间最值,区间翻转,区间加,区间求和等区间操作。

使用FHQTreap 可以方便的对序列进行操作,只需要将分裂方式从按值分裂改为按大小分裂即可。

当然,为了保证时间复杂度,需要配合懒标记和下放懒标记的函数使用。

void split(int p,int k,int &l,int &r){
    if(p) push_down(p);
    else{l=r=0;return ;}
    if(a[a[p].ch[0]].siz<k){
        l=p;split(a[p].ch[1],k-a[a[p].ch[0]].siz-1,a[p].ch[1],r);
    }
    else{
        r=p;split(a[p].ch[0],k,l,a[p].ch[0]);
    }
    push_up(p);
}

要进行对\([l,r]\)的区间操作时只需要将树分裂成三颗,对中间的树进行操作,最后合并三颗树即可。

void operate(int l,int r,int k){
    int x,y,z;
    split(rt,r,x,z);
    split(x,l-1,x,y);
    operate_t(y,k);
    rt=merge(merge(x,y),z);
}

懒标记的使用基于题目决定。值得注意的是,在执行任何递归子树的操作前都要下放懒标记。

int merge(int p,int q){
    if(!p||!q) return p+q;
    if(a[p].key<a[q].key){
        push_down(p);a[p].ch[1]=merge(a[p].ch[1],q);
        push_up(p);return p;
    }
    else{
        push_down(q);a[q].ch[0]=merge(p,a[q].ch[0]);
        push_up(q);return q;
    }
}

而下放懒标记的方法也很简单,跟线段树差不多。

void push_down(int p){
    if(a[p].t){
        if(a[p].ch[0]) operate_t(a[p].ch[0]);
        if(a[p].ch[1]) operate_t(a[p].ch[1]);
        a[p].t=0;
    }
}

operatr_t()函数以及push_up()函数等根据题目来实现。

维护区间的最大子列和(必须至少选一个数),区间和,区间翻转,区间修改:

struct FHQn{
    int ch[2];
    int val,siz,key;
    int t1,t2,t3;
    int sum,maxl,maxr,maxs;
};

void push_up(int p){
    a[p].sum=a[a[p].ch[0]].sum+a[a[p].ch[1]].sum+a[p].val;
    a[p].siz=a[a[p].ch[0]].siz+a[a[p].ch[1]].siz+1;
    a[p].maxl=max({a[a[p].ch[0]].maxl,a[a[p].ch[0]].sum+a[a[p].ch[1]].maxl,0});
    a[p].maxr=max({a[a[p].ch[1]].maxr,a[a[p].ch[1]].sum+a[a[p].ch[0]].maxr,0});
    a[p].maxs=max(a[p].val,a[a[p].ch[0]].maxr+a[p].val+a[a[p].ch[1]].maxl);
    if(a[p].ch[0]) a[p].maxs=max(a[p].maxs,a[a[p].ch[0]].maxs);
    if(a[p].ch[1]) a[p].maxs=max(a[p].maxs,a[a[p].ch[1]].maxs);
}

void reverse_t(int p){
    if(!p) return ;
    a[p].t1^=1;
    swap(a[p].ch[0],a[p].ch[1]);
    swap(a[p].maxl,a[p].maxr);
}

void change_t(int p,int k){
    if(!p) return ;
    a[p].val=a[p].t3=k;a[p].t2=1;
    a[p].sum=a[p].siz*k;
    a[p].maxs=max(a[p].sum,k);
    a[p].maxl=a[p].maxr=max(a[p].sum,0);
}

void push_down(int p){
    if(!p) return ;
    if(a[p].t1){
        reverse_t(a[p].ch[0]);
        reverse_t(a[p].ch[1]);
        a[p].t1=0;
    }
    if(a[p].t2){
        change_t(a[p].ch[0],a[p].t3);
        change_t(a[p].ch[1],a[p].t3);
        a[p].t2=a[p].t3=0;
    }
}

平衡树的应用

  • 如果我们将上文的维护集合的平衡树和维护序列的平衡树合并,会怎么样?

树套树!

树套树可以求区间前驱,区间后继,区间排名,区间查询某数的排名,区间查询排名对应的数等,当然,也支持单点修改,区间最值等操作。

树套树有很多种,如线段树套线段树,平衡树套线段树,线段树套平衡树,树状数组套权值线段树,分块套树状数组等。

树套树中存在外层树和内层树的概念,外层树包括线段树,平衡树,树状数组,分块等,内层树包括权值线段树,平衡树,树状数组,STL的set等。所以组合起来至少有16种树套树

当然,不同的树套树对于不同的操作有着不同的复杂度:

线段树套权值线段树线段树:均为\(O(\log^2n)\)

线段树套平衡树:除了查询区间内排名对应的数为\(O(\log^3n)\),其他均为\(O(\log^2n)\)

树状数组套权值线段树:均为\(O(\log^2n)\)

大部分复杂度为\(O(\log^2 n)\)的原因是进行操作时,操作在外层树被划分成了\(O(\log n)\)个区间,而对于每个区间,内层树都进行了\(O(\log n)\)次操作,所以合起来时\(O(\log^2n)\)的。

当然,树套树的码量相对较大,正常写法在\(4.5kb\)左右。

  • 维护奇怪的东西

序列平衡树可以维护序列,而DP 正是基于序列实现的,所以用平衡树可以动态的维护DP数组。

但往往DP 与平衡树结合的题目是用平衡树动态维护斜率优化的凸包。

于是这又让平衡树和计算几何扯上了关系。

类似维护DP,平衡树也可以动态维护哈希数组。