数据结构:树套树-替罪羊树套权值线段树

BZOJ3065

本题是在BZOJ上的处女A,实在不应该拿这样一道题来开头

平衡树套线段树应该是树套树问题里比较难的一种了,当然我记得还有一个替罪羊树套Trie树的题,我是不信自己能写出来的。

外层的平衡树来实现区间值得插入和修改,其实如果外层的插入是只在中间插入的话,可以不用平衡树的

为了避免旋转对内层数据结构的影响,这里的外层平衡树是不能够旋转的,像这种外层平衡树的题,外层的树一般都是替罪羊树,像无旋Treap一般是用来实现可持久化平衡树的

总的来说,各种树有各种树的用途,我如果只是一道单纯的平衡树的题,就根本没有必要用裸的替罪羊树维护了是不是

下面我来完整地解析一下实现过程:

const int maxn=70005;
const float alpha=0.75;
int n,m,root,gtmp,sz,lastans;
int v[maxn],dfn[maxn],rt[maxn],lch[maxn],rch[maxn];
//rt是替罪羊节点存的每一个线段树的根节点 
struct seg{int l,r,sum;}a[10000005];
vector<int> rec,t,p;

先来看声明部分,alpha是替罪羊平衡因子,记住如果是常数一定不要用int,会崩的

区间长度为n,m个询问,替罪羊树的根节点咱们叫root,gtmp是和重构子树有关的临时变量,sz是线段树节点个数,lastans是跟题目有关的一个变量

这里的替罪羊树是SOA,v是节点的值,dfn是节点的ID用来节省空间,rt是每一个替罪羊节点对应的权值线段树的根节点,lch和rch是替罪羊左右子树的ID

权值线段树是AOS,l存左子树ID,r存右子树ID,sum在这里就是子树的节点个数了,因为有这个sum在替罪羊树中的size就不必了

这里的权值线段树虽然是完全二叉树但是存的时候是存成数组链的,不支持完全二叉树的2n,2n+1的操作,其实主要原因是空间不够

rec用来存回收来的线段树节点,为了省空间,t是用来存替罪羊子树(或者说一个区间)所有的权值线段树节点,p用来存这个区间每一个节点的值

t和p是在查询的时候二分查询权值线段树的时候用的,因为这个线段树是用链存的,所以查询部分还是不太会,只是大概理解了个思路

void build(int &k,int l,int r)
{
    //cout<<"###5"<<endl;
    if(l>r) return;
    if(l==r)
    {
        k=dfn[l];
        insert(rt[k],0,70000,v[k],1);  //在替罪羊树的叶子节点插入权值线段树 
        return;
    }
    int mid=(l+r)>>1;k=dfn[mid];
    build(lch[k],l,mid-1);build(rch[k],mid+1,r);
    for(int i=l;i<=r;i++) insert(rt[k],0,70000,v[dfn[i]],1); 
}

这是建树操作,建替罪羊树,在建的过程中每一个节点都插入了一个权值线段树,这个线段树是一个一个插出来的,其中insert的最后一个参数就是所谓的权值

然后我们立刻看一下权值线段树怎么建的

void insert(int &k,int l,int r,int val,int f)  //每一个替罪羊节点插入权值线段树 
{
    //cout<<"###4"<<endl;
    if(k==0) k=newnode();
    if(l==r) {a[k].sum+=f;return;}
    int mid=(l+r)>>1;
    if(val<=mid) insert(a[k].l,l,mid,val,f);
    else insert(a[k].r,mid+1,r,val,f);
    a[k].sum=a[a[k].l].sum+a[a[k].r].sum;
    if(a[k].sum==0) reclaim(k); 
} 

可以很明显看出来是线段树递归建树了,如果插完之后发现没有权值,直接回收

inline int newnode()
{
    //cout<<"###2"<<endl;
    if(rec.size()==0) return ++sz;
    else
    {
        int tmp=rec.back();
        rec.pop_back();
        return tmp;
    }
}

向(回收站)申请一个线段树节点空间

看一下怎么拆掉没用的线段树的

void reclaim(int &k)  //回收叶子的线段树空间
{
    //cout<<"###3"<<endl;
    if(k==0) return;
    rec.push_back(k);
    reclaim(a[k].l),reclaim(a[k].r);
    a[k].sum=0;k=0; 
}

查询就比较恶心了

void query(int k,int l,int r)  //提取出要查询的权值线段树?
{
    //cout<<"###8"<<endl;
    int L=a[rt[lch[k]]].sum,R=a[rt[k]].sum;
    if(l==1&&r==R) {t.push_back(rt[k]);return;}  //要查询的区间映射到了权值线段树 
    if(l<=L+1&&r>=L+1) p.push_back(v[k]);  //当前根节点在要查询的区间里 
    if(r<=L) query(lch[k],l,r);
    else if(l>L+1) query(rch[k],l-L-1,r-L-1);
    else
    {
        if(l<=L) query(lch[k],l,L);
        if(R>L+1) query(rch[k],1,r-L-1);
    }
}
int solve_query(int L,int R,int K) //二分权值线段树查询区间第k大 ?
{
    //cout<<"###9"<<endl;
    query(root,L,R);K--;
    int l=0,r=70000,s1=t.size(),s2=p.size();
    while(l<r)
    {
        int mid=(l+r)>>1,sum=0;
        for(int i=0;i<s1;i++) sum+=a[a[t[i]].l].sum;
        for(int i=0;i<s2;i++)
            if(p[i]>=l&&p[i]<=mid) sum++;
        if(K<sum)
        {
            for(int i=0;i<s1;i++) t[i]=a[t[i]].l;
            r=mid;
        }
        else
        {
            for(int i=0;i<s1;i++) t[i]=a[t[i]].r;
            l=mid+1;K-=sum;
        }
    } 
    t.clear(),p.clear();
    return l;
}

上面那个查询是用来把替罪羊子树对应的权值线段树都翟出来的,底下这个函数就是二分这个翟出来的权值线段树然后二分求第k大的

说到这里我可能知道了多颗权值线段树能够维护静态第k大,或者说,线段树套权值线段树?一会儿去试试,感觉比这个要简单一些

接下来说修改

int modify(int k,int x,int val)
{
    //cout<<"###8"<<endl;
    insert(rt[k],0,70000,val,1);  //在根节点权值线段树插入
    int t,L=a[rt[lch[k]]].sum;
    if(L+1==x) {t=v[k];v[k]=val;}
    else if(L>=x) t=modify(lch[k],x,val);
    else t=modify(rch[k],x-L-1,val);
    insert(rt[k],0,70000,t,-1);
    return t; 
}

修改就是把从根节点到这个修改点所影响的所有线段树进行统一的,插新数,删旧数

最后就是迷之插入了

void insert(int &k,int x,int val)
{
    //cout<<"###10"<<endl;
    if(k==0)  //叶子节点 
    {
        k=++n;
        insert(rt[k],0,70000,val,1);
        v[k]=val;
        return;
    }
    insert(rt[k],0,70000,val,1);
    int L=a[rt[lch[k]]].sum;  //得到当前平衡树节点在权值线段树的坐标 
    if(L>=x) insert(lch[k],x,val);
    else insert(rch[k],x-L-1,val);
    if(a[rt[k]].sum*alpha>max(a[rt[lch[k]]].sum,a[rt[rch[k]]].sum))
    {
        if(gtmp)
        {
            if(lch[k]==gtmp) rebuild(lch[k]);
            else rebuild(rch[k]);
            gtmp=0;
        }
    }
    else gtmp=k;
}

这就是往平衡树里插东西了,插完之后通过权值线段树的sum来看有没有平衡啊,如果没有平衡的话,就重构把

重构的时候不是把替罪羊树变成完全二叉树就完了,还得把里面套的线段树维护好,这里的话

void del(int &k)  //回收替罪羊树 
{
    //cout<<"###6"<<endl;
    if(k==0) return;
    reclaim(rt[k]);  //回收线段树 
    del(lch[k]);p.push_back(k);del(rch[k]);
    k=0;
}
void rebuild(int &k)
{
    //cout<<"###7"<<endl;
    del(k);int s1=p.size();
    for(int i=1;i<=s1;i++) dfn[i]=p[i-1];
    build(k,1,s1);
    p.clear(); 
    
}

线段树直接暴力插进去了,如果要是线段树合并的话,可能还要学一学了,线段树合并是O(logn)的

感谢黄学长

附完整代码:

  1 #include<cstdio>
  2 #include<vector>
  3 #include<iostream>
  4 #include<algorithm>
  5 using namespace std;
  6 const int maxn=70005;
  7 const float alpha=0.75;
  8 int n,m,root,gtmp,sz,lastans;
  9 int v[maxn],dfn[maxn],rt[maxn],lch[maxn],rch[maxn];
 10 //rt是替罪羊节点存的每一个线段树的根节点 
 11 struct seg{int l,r,sum;}a[10000005];
 12 vector<int> rec,t,p;
 13 inline int read()
 14 {
 15     //cout<<"###1"<<endl;
 16     int x=0;
 17     char ch=getchar();
 18     while(ch<'0'||ch>'9') ch=getchar();
 19     while(ch>='0'&&ch<='9') {x=x*10+ch-'0';ch=getchar();}
 20     return x;
 21 }
 22 inline int newnode()
 23 {
 24     //cout<<"###2"<<endl;
 25     if(rec.size()==0) return ++sz;
 26     else
 27     {
 28         int tmp=rec.back();
 29         rec.pop_back();
 30         return tmp;
 31     }
 32 }
 33 void reclaim(int &k)  //回收叶子的线段树空间
 34 {
 35     //cout<<"###3"<<endl;
 36     if(k==0) return;
 37     rec.push_back(k);
 38     reclaim(a[k].l),reclaim(a[k].r);
 39     a[k].sum=0;k=0; 
 40 }
 41 void insert(int &k,int l,int r,int val,int f)  //每一个替罪羊节点插入权值线段树 
 42 {
 43     //cout<<"###4"<<endl;
 44     if(k==0) k=newnode();
 45     if(l==r) {a[k].sum+=f;return;}
 46     int mid=(l+r)>>1;
 47     if(val<=mid) insert(a[k].l,l,mid,val,f);
 48     else insert(a[k].r,mid+1,r,val,f);
 49     a[k].sum=a[a[k].l].sum+a[a[k].r].sum;
 50     if(a[k].sum==0) reclaim(k); 
 51 } 
 52 void build(int &k,int l,int r)
 53 {
 54     //cout<<"###5"<<endl;
 55     if(l>r) return;
 56     if(l==r)
 57     {
 58         k=dfn[l];
 59         insert(rt[k],0,70000,v[k],1);  //在替罪羊树的叶子节点插入权值线段树 
 60         return;
 61     }
 62     int mid=(l+r)>>1;k=dfn[mid];
 63     build(lch[k],l,mid-1);build(rch[k],mid+1,r);
 64     for(int i=l;i<=r;i++) insert(rt[k],0,70000,v[dfn[i]],1); 
 65 }
 66 void del(int &k)  //回收替罪羊树 
 67 {
 68     //cout<<"###6"<<endl;
 69     if(k==0) return;
 70     reclaim(rt[k]);  //回收线段树 
 71     del(lch[k]);p.push_back(k);del(rch[k]);
 72     k=0;
 73 }
 74 void rebuild(int &k)
 75 {
 76     //cout<<"###7"<<endl;
 77     del(k);int s1=p.size();
 78     for(int i=1;i<=s1;i++) dfn[i]=p[i-1];
 79     build(k,1,s1);
 80     p.clear(); 
 81     
 82 }
 83 int modify(int k,int x,int val)
 84 {
 85     //cout<<"###8"<<endl;
 86     insert(rt[k],0,70000,val,1);  //在根节点权值线段树插入
 87     int t,L=a[rt[lch[k]]].sum;
 88     if(L+1==x) {t=v[k];v[k]=val;}
 89     else if(L>=x) t=modify(lch[k],x,val);
 90     else t=modify(rch[k],x-L-1,val);
 91     insert(rt[k],0,70000,t,-1);
 92     return t; 
 93 }
 94 void query(int k,int l,int r)  //提取出要查询的权值线段树?
 95 {
 96     //cout<<"###8"<<endl;
 97     int L=a[rt[lch[k]]].sum,R=a[rt[k]].sum;
 98     if(l==1&&r==R) {t.push_back(rt[k]);return;}  //要查询的区间映射到了权值线段树 
 99     if(l<=L+1&&r>=L+1) p.push_back(v[k]);  //当前根节点在要查询的区间里 
100     if(r<=L) query(lch[k],l,r);
101     else if(l>L+1) query(rch[k],l-L-1,r-L-1);
102     else
103     {
104         if(l<=L) query(lch[k],l,L);
105         if(R>L+1) query(rch[k],1,r-L-1);
106     }
107 }
108 int solve_query(int L,int R,int K) //二分权值线段树查询区间第k大 ?
109 {
110     //cout<<"###9"<<endl;
111     query(root,L,R);K--;
112     int l=0,r=70000,s1=t.size(),s2=p.size();
113     while(l<r)
114     {
115         int mid=(l+r)>>1,sum=0;
116         for(int i=0;i<s1;i++) sum+=a[a[t[i]].l].sum;
117         for(int i=0;i<s2;i++)
118             if(p[i]>=l&&p[i]<=mid) sum++;
119         if(K<sum)
120         {
121             for(int i=0;i<s1;i++) t[i]=a[t[i]].l;
122             r=mid;
123         }
124         else
125         {
126             for(int i=0;i<s1;i++) t[i]=a[t[i]].r;
127             l=mid+1;K-=sum;
128         }
129     } 
130     t.clear(),p.clear();
131     return l;
132 }
133 void insert(int &k,int x,int val)
134 {
135     //cout<<"###10"<<endl;
136     if(k==0)  //叶子节点 
137     {
138         k=++n;
139         insert(rt[k],0,70000,val,1);
140         v[k]=val;
141         return;
142     }
143     insert(rt[k],0,70000,val,1);
144     int L=a[rt[lch[k]]].sum;  //得到当前平衡树节点在权值线段树的坐标 
145     if(L>=x) insert(lch[k],x,val);
146     else insert(rch[k],x-L-1,val);
147     if(a[rt[k]].sum*alpha>max(a[rt[lch[k]]].sum,a[rt[rch[k]]].sum))
148     {
149         if(gtmp)
150         {
151             if(lch[k]==gtmp) rebuild(lch[k]);
152             else rebuild(rch[k]);
153             gtmp=0;
154         }
155     }
156     else gtmp=k;
157 }
158 int main()
159 {
160     n=read();
161     for(int i=1;i<=n;i++) v[i]=read();
162     for(int i=1;i<=n;i++) dfn[i]=i;
163     build(root,1,n);
164     m=read();
165     int x,y,k;
166     char tmp[5];
167     while(m--)
168     {
169         scanf("%s",tmp);
170         x=read();y=read();x^=lastans;y^=lastans;
171         switch(tmp[0])
172         {
173             case 'Q':k=read();k^=lastans;lastans=solve_query(x,y,k);printf("%d
",lastans);break;
174             case 'M':modify(root,x,y);break;
175             case 'I':gtmp=0;insert(root,x-1,y);
176                 if(gtmp) {gtmp=0;rebuild(root);}
177                 break;
178         }
179     }
180     return 0;
181 }
原文地址:https://www.cnblogs.com/aininot260/p/9367918.html