树套树学习笔记

· · 算法·理论

数据结构唉。

前言

所谓树套树,并不是一个具体的算法,而是一类算法的总称。

具体来说是维护两个树形数据结构套在一起就叫树套树。(外层的树的每一个节点都代表内层的一棵树。)

所以祂可以是线段树套线段树,线段树套平衡树,树状数组套主席树等等。

由于作者数据结构实力薄弱,本文仅是一个学完模板后的一点小小记录。并没有能力去做一些大困难题。

例题

P3380 【模板】树套树

:::info[题意简述]{open} 给定一个序列 a

你发现相对于平衡树的模板从全局查询变成了区间查询,同时加入了修改。那么我们考虑建一棵线段树,每一个结点表示一个区间,而对于每个区间建立一棵平衡树维护该区间里的信息,具体地每棵平衡树存储的是对应区间里当前的所有序列值组成的可重集。那么树套树的结构就初步完成了。

依次考虑操作:

那么算一下复杂度吧!线段树复杂度是 \mathcal{O(n\log n)},对于单个区间操作平衡树的单次复杂度是 \mathcal{O(\log n)},最后二分答案再加 \mathcal{O(\log V)},其中 V 代表值域。那么总的复杂度约是 \mathcal{O(n\log^3 n)}。我使用 FHQ-Treap 维护的平衡树,那么还要带上一个大常数哈哈。超级复杂度属于是。

具体代码实现还是很繁复,但逻辑是清晰的。

:::success[代码]

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5,inf=2147483647;
int n,m,tot,b[N],RT[N<<2];//线段树上使用一棵平衡树的根代表该平衡树 
struct fhq{int val,siz,pri,lc,rc;}a[N*50];
//以下都是fhq-treap模板基操 
void ps(int cur){return a[cur].siz=a[a[cur].lc].siz+a[a[cur].rc].siz+1,void();}
void split(int rt,int k,int &x,int &y){//分裂 
    if(!rt)return x=y=0,void();
    if(a[rt].val<=k)x=rt,split(a[rt].rc,k,a[rt].rc,y);
    else y=rt,split(a[rt].lc,k,x,a[rt].lc);
    ps(rt);
    return;
}
int uni(int x,int y){//合并 
    if(!x||!y)return x+y;
    if(a[x].pri>a[y].pri)return a[x].rc=uni(a[x].rc,y),ps(x),x;
    else return a[y].lc=uni(x,a[y].lc),ps(y),y;
}
void add(int x){return tot++,a[tot]={x,1,rand(),0,0},void();}//加新点 
int ins(int x,int &root){//插入x,注意这里传参root(操作的root不同所以不能写全局,但root会改变所以加上&符号) 
    int L,R;
    split(root,x,L,R),add(x);
    root=uni(uni(L,tot),R);
    return tot;
}
int del(int x,int &root){//删除x,注意这里也要传参 
    int L,M,R;
    split(root,x,L,R),split(L,x-1,L,M);
    M=uni(a[M].lc,a[M].rc),root=uni(uni(L,M),R);
    return root;
}
int calc1(int x,int root){//计算x的排名(比x小的数的数量) 
    int xt,yt;
    split(root,x-1,xt,yt);
    int tmp=a[xt].siz-1;
    return uni(xt,yt),tmp;
}
int calc2(int rt,int k){//计算排名为k的数 
    if(a[a[rt].lc].siz+1==k)return rt;
    if(a[a[rt].lc].siz>=k)return calc2(a[rt].lc,k);
    return calc2(a[rt].rc,k-a[a[rt].lc].siz-1);
}
int g_pre(int x,int root){//找前驱 
    int xt,yt;
    split(root,x-1,xt,yt);
    int tmp=a[calc2(xt,a[xt].siz)].val;
    return uni(xt,yt),tmp;
}
int g_suf(int x,int root){//找后继 
    int xt,yt;
    split(root,x,xt,yt);
    int tmp=a[calc2(yt,1)].val;
    return uni(xt,yt),tmp;
} 
//注意calc1,calc2,g_pre,g_suf是可以不用写&root的,因为没有改变树的结构只是单纯分裂一下找个信息,所以root不会改变 
#define ls (cur<<1)
#define rs (cur<<1|1)
#define mid (l+r>>1) 
void bd(int cur,int l,int r){
    ins(inf,RT[cur]),ins(-inf,RT[cur]);//初始插入哨兵节点以防有时找不到信息出现RE等错误 
    for(int i=l;i<=r;i++)ins(b[i],RT[cur]);//插入这个区间所有的元素 
    if(l==r)return;
    bd(ls,l,mid),bd(rs,mid+1,r);
}
void upd(int cur,int l,int r,int x,int val){
    del(b[x],RT[cur]),ins(val,RT[cur]);
    if(l==r)return;
    if(x<=mid)upd(ls,l,mid,x,val);
    else upd(rs,mid+1,r,x,val);
}
int q_rk(int cur,int l,int r,int x,int y,int val){
    if(x<=l&&r<=y)return calc1(val,RT[cur]);
    int res=0;
    if(x<=mid)res+=q_rk(ls,l,mid,x,y,val);
    if(y>mid)res+=q_rk(rs,mid+1,r,x,y,val);
    return res;
}
int q_pre(int cur,int l,int r,int x,int y,int val){
    if(x<=l&&r<=y)return g_pre(val,RT[cur]);
    int res=-inf;
    if(x<=mid)res=max(res,q_pre(ls,l,mid,x,y,val));
    if(y>mid)res=max(res,q_pre(rs,mid+1,r,x,y,val));
    return res;
}
int q_suf(int cur,int l,int r,int x,int y,int val){
    if(x<=l&&r<=y)return g_suf(val,RT[cur]);
    int res=inf;
    if(x<=mid)res=min(res,q_suf(ls,l,mid,x,y,val));
    if(y>mid)res=min(res,q_suf(rs,mid+1,r,x,y,val));
    return res;
}
int main(){
    srand(time(0));
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>b[i];
    bd(1,1,n);
    while(m--){
        int op,x,y,z;
        cin>>op>>x>>y;
        if(op!=3)cin>>z;
        if(op==1)cout<<q_rk(1,1,n,x,y,z)+1<<"\n";//排名记得+1 
        else if(op==2){
            int lt=-1,rt=1e8+1;
            while(lt+1<rt){
                int md=lt+rt>>1;
                if(q_rk(1,1,n,x,y,md)+1<=z)lt=md;
                else rt=md;
            }
            cout<<lt<<"\n";
        } 
        else if(op==3)upd(1,1,n,x,y),b[x]=y;
        else if(op==4)cout<<q_pre(1,1,n,x,y,z)<<"\n";
        else cout<<q_suf(1,1,n,x,y,z)<<"\n";
    }
    return 0;
}

:::

P3437 [POI 2006] TET-Tetris 3D

:::info[题意转化]{open} 其实就是求解二维矩阵内点权的最大值 w_{\max},然后再将这个矩阵所有点的权值 ww\leftarrow \max(w,w_{\max}+h)。 :::

这个是线段树套线段树(二维线段树)。

其实就是外层线段树的每一个节点都维护一个线段树啦。

一般来说二维线段树的外层是不好进行标记上传下传的,所以我们要进行的操作是标记永久化。

首先考虑一维操作,区间求最大值以及区间赋值最大值加上某个正数。维护两个东西,一个是区间最大值 tr_x,一个是标记 tag_x

对于修改操作,对遍历到的每个节点尝试更新 tr_x,因为这个线段树大区间一定与操作区间有交。在遇到被操作区间完全包含的线段树区间,更新其标记 tag_x\to \max(tag_x,val)

对于询问操作,就是反过来的思维。对于被操作区间完全包含的线段树区间,显然可以贡献答案 tr_x。在遇到有交但并不包含的线段树区间是,就用 tag_x 更新(因为有交,而这整个区间都有值 tag_x,所以询问区间一定能被一部分 tag_x 覆盖到,所以可以更新)。

对于二维操作,在外层的线段树如法炮制即可。

:::success[代码]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
int D,S,q;
#define ls (cur<<1)
#define rs (cur<<1|1)
#define mid (l+r>>1)
struct node{
    int tr[N<<2],tg[N<<2];
    void upd(int cur,int l,int r,int x,int y,int val){
        tr[cur]=max(tr[cur],val);
        if(x<=l&&r<=y)return tg[cur]=max(tg[cur],val),void();
        if(x<=mid)upd(ls,l,mid,x,y,val);
        if(y>mid)upd(rs,mid+1,r,x,y,val);
        return;
    }
    int qy(int cur,int l,int r,int x,int y){
        if(x<=l&&r<=y)return tr[cur];
        int res=tg[cur];
        if(x<=mid)res=max(res,qy(ls,l,mid,x,y));
        if(y>mid)res=max(res,qy(rs,mid+1,r,x,y));
        return res;
    }
}tr[N<<2],tg[N<<2];
void upd(int cur,int l,int r,int x,int y,int px,int py,int val){
    tr[cur].upd(1,1,S,px,py,val);
    if(x<=l&&r<=y)return tg[cur].upd(1,1,S,px,py,val),void();
    if(x<=mid)upd(ls,l,mid,x,y,px,py,val);
    if(y>mid)upd(rs,mid+1,r,x,y,px,py,val);
    return;
}
int qy(int cur,int l,int r,int x,int y,int px,int py){
    if(x<=l&&r<=y)return tr[cur].qy(1,1,S,px,py);
    int res=tg[cur].qy(1,1,S,px,py);
    if(x<=mid)res=max(res,qy(ls,l,mid,x,y,px,py));
    if(y>mid)res=max(res,qy(rs,mid+1,r,x,y,px,py));
    return res;
}
signed main(){
    cin>>D>>S>>q,D++,S++;
    while(q--){
        int d,s,w,x,y;
        cin>>d>>s>>w>>x>>y,x++,y++;
        upd(1,1,D,x,x+d-1,y,y+s-1,qy(1,1,D,x,x+d-1,y,y+s-1)+w);
    }
    cout<<qy(1,1,D,1,D,1,S);
    return 0;
}

:::

后记

本人确实对这一块数据结构了解的肤浅了一点,所以写的学习笔记非常之粗糙。

u1s1 确实不是很喜欢树套树,因为其大复杂度,大常数和大空间,还有其史中史的代码。但是了解其思想确实是必要的。那么就写到这里。