数据结构:替罪羊树

替罪羊树作为平衡树家族里比较简单的一员,效率还是很不错的

只要不是维护序列之类的需要提取子树进行操作的问题,选择高效率的重量平衡树是无可非议的

我们可以用一个标准:需不需要采用旋转操作来对重量平衡树进行一个简单的分类:

没有采用旋转机制的有:跳表和替罪羊树

采用旋转机制的有:Treap

所有采用旋转机制的平衡树都有这么一个弊端:在平衡树的每个节点上维护一个集合,来存储子树内部所有的数,此时单次旋转操作可能有O(n)的时间复杂度

那么什么是重量平衡树?如果你把势能的概念引入到平衡树的节点上面去,就比较容易理解了

在这种情况下,完全二叉树的势能是最低的,对于那些势能高的子树,我们或旋转或拍平重构来使它接近甚至成为完全二叉树结构,这应该就是重量平衡树的本质了(其实吧,平衡树的话,应该都是这样的)

然后开始介绍正题:替罪羊树的平衡机理:

对于某个0.5<=alpha<=1满足size(lch(x))<=alpha*size(x)并且size(rch(x))<=alpha*size(x),即这个节点的两棵子树的size都不超过以该节点为根的子树的size,那么就称这个子树(或节点)是平衡的

然后如果不平衡的话,直接拍平之后重构为完全二叉树就好了

然后开始说代码,我的数据结构的代码风格,总体来说还是比较凌乱的,后期一定会修整的,一定会修整的。

const int INF=1000000000;
const int maxn=2000005;
const double al=0.75;
int n;
struct Tree
{
    int fa;
    int size;
    int num;
    int ch[2];
}t[maxn];
int cnt;
int root;
int node[maxn];
int sum;

在这里al就是平衡因子,size是该子树包含的节点个数,num是值(Splay中我用的是v),cnt记录节点个数,node是个存点坐标的临时数组,其下标用sum来指代

然后再来说一说建树操作,一般我们的建树操作是指给定一个装满了数的数组,然后调用建树函数,以这个数组为依托递归建树,就像这个函数:

int build(int l,int r)
{
    if(l>r)
        return 0;
    int mid=(l+r)/2;
    int x=node[mid];
    t[t[x].ch[0]=build(l,mid-1)].fa=x;
    t[t[x].ch[1]=build(mid+1,r)].fa=x;
    t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1;
    return x;
}

但其实为了方便,我们大可直接一个一个数往数据结构里面插就好了

这种操作在替罪羊树中只用于在重构子树的时候临时存一下子树拍平之后的那些点,我们引出插入函数:

void insert(int x)
{
    int o=root;
    int cur=++cnt;
    t[cur].size=1;
    t[cur].num=x;
    while(1)
    {
        t[o].size++;
        bool son=(x>=t[o].num);
        if(t[o].ch[son])
            o=t[o].ch[son];
        else
        {
            t[t[o].ch[son]=cur].fa=o;
            break;
        }
    }
    int flag=0;
    for(int i=cur;i;i=t[i].fa)
    if(!balance(i))
        flag=i;
    if(flag)
        rebuild(flag);
}

可以看到如果插入之后不平衡了,就要完成重构了,重构子树为完全二叉树

这里给出判断是否需要重构的函数,就是本文开头给出的那个公式

bool balance(int x)
{
    return (double)t[x].size*al>=(double)t[t[x].ch[0]].size
    &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size;
}

如果真的需要重构的话,就调用重构函数进行重构,重构操作是拍成链然后重建子树,之后还接回去

void rebuild(int x)
{
    sum=0;
    recycle(x);
    int fa=t[x].fa;
    int son=(t[t[x].fa].ch[1]==x);
    int cur=build(1,sum);
    t[t[fa].ch[son]=cur].fa=fa;
    if(x==root)
        root=cur;
}

这里给出拍成链的函数,这里就用到刚才说的那个node和sum了

void recycle(int x)
{
    if(t[x].ch[0])
        recycle(t[x].ch[0]);
    node[++sum]=x;
    if(t[x].ch[1])
        recycle(t[x].ch[1]);
}

插入的问题说完了,然后说说删除,其实替罪羊这个名字就是因为这个删除操作而来的

void erase(int x)
{
    if(t[x].ch[0]&&t[x].ch[1])
    {
        int cur=t[x].ch[0];
        while(t[cur].ch[1])
            cur=t[cur].ch[1];
        t[x].num=t[cur].num;
        x=cur;
    }
    int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1];
    int k=(t[t[x].fa].ch[1]==x);
    t[t[t[x].fa].ch[k]=son].fa=t[x].fa;
    for(int i=t[x].fa;i;i=t[i].fa)
        t[i].size--;
    if(x==root)
        root=son;
}

替罪羊树中的删除操作,就是用被删除节点的左子树的最后一个节点或者右子树的第一个节点来顶替被删除节点的位置

查询操作的话,具备一般平衡树的一些基本功能:

查询x数的排名

查询排名为x的数(这个Splay里面写了,剩下两个都没有写,以后再完善吧)

求x的前驱和后继

int get_rank(int x)
{
    int o=root,ans=0;
    while(o)
    {
        if(t[o].num<x)
            ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1];
        else
            o=t[o].ch[0];
    }
    return ans;
}
int get_kth(int x)
{
    int o=root;
    while(1)
    {
        if(t[t[o].ch[0]].size==x-1)
            return o;
        else if(t[t[o].ch[0]].size>=x)
            o=t[o].ch[0];
        else
            x-=t[t[o].ch[0]].size+1,o=t[o].ch[1];
    }
    return o;
}
int get_front(int x)
{
    int o=root,ans=-INF;
    while(o)
    {
        if(t[o].num<x)
            ans=max(ans,t[o].num),o=t[o].ch[1];
        else
            o=t[o].ch[0];
    }
    return ans;
}
int get_behind(int x)
{
    int o=root,ans=INF;
    while(o)
    {
        if(t[o].num>x)
            ans=min(ans,t[o].num),o=t[o].ch[0];
        else
            o=t[o].ch[1];
    }
    return ans;
}

其实查询操作对各个树而言大同小异,只不过,我目前知道的,Splay干什么都要splay到根节点一下子

最后,我们给出替罪羊树的完整实现:

  1 #include<iostream>
  2 #include<algorithm>
  3 using namespace std;
  4 const int INF=1000000000;
  5 const int maxn=2000005;
  6 const double al=0.75;
  7 int n;
  8 struct Tree
  9 {
 10     int fa;
 11     int size;
 12     int num;
 13     int ch[2];
 14 }t[maxn];
 15 int cnt;
 16 int root;
 17 int node[maxn];
 18 int sum;
 19 bool balance(int x)
 20 {
 21     return (double)t[x].size*al>=(double)t[t[x].ch[0]].size
 22     &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size;
 23 }
 24 void recycle(int x)
 25 {
 26     if(t[x].ch[0])
 27         recycle(t[x].ch[0]);
 28     node[++sum]=x;
 29     if(t[x].ch[1])
 30         recycle(t[x].ch[1]);
 31 }
 32 int build(int l,int r)
 33 {
 34     if(l>r)
 35         return 0;
 36     int mid=(l+r)/2;
 37     int x=node[mid];
 38     t[t[x].ch[0]=build(l,mid-1)].fa=x;
 39     t[t[x].ch[1]=build(mid+1,r)].fa=x;
 40     t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1;
 41     return x;
 42 }
 43 void rebuild(int x)
 44 {
 45     sum=0;
 46     recycle(x);
 47     int fa=t[x].fa;
 48     int son=(t[t[x].fa].ch[1]==x);
 49     int cur=build(1,sum);
 50     t[t[fa].ch[son]=cur].fa=fa;
 51     if(x==root)
 52         root=cur;
 53 }
 54 void insert(int x)
 55 {
 56     int o=root;
 57     int cur=++cnt;
 58     t[cur].size=1;
 59     t[cur].num=x;
 60     while(1)
 61     {
 62         t[o].size++;
 63         bool son=(x>=t[o].num);
 64         if(t[o].ch[son])
 65             o=t[o].ch[son];
 66         else
 67         {
 68             t[t[o].ch[son]=cur].fa=o;
 69             break;
 70         }
 71     }
 72     int flag=0;
 73     for(int i=cur;i;i=t[i].fa)
 74     if(!balance(i))
 75         flag=i;
 76     if(flag)
 77         rebuild(flag);
 78 }
 79 int get_num(int x)
 80 {
 81     int o=root;
 82     while(1)
 83     {
 84         if(t[o].num==x)
 85             return o;
 86         else
 87             o=t[o].ch[t[o].num<x];
 88     }
 89 }
 90 void erase(int x)
 91 {
 92     if(t[x].ch[0]&&t[x].ch[1])
 93     {
 94         int cur=t[x].ch[0];
 95         while(t[cur].ch[1])
 96             cur=t[cur].ch[1];
 97         t[x].num=t[cur].num;
 98         x=cur;
 99     }
100     int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1];
101     int k=(t[t[x].fa].ch[1]==x);
102     t[t[t[x].fa].ch[k]=son].fa=t[x].fa;
103     for(int i=t[x].fa;i;i=t[i].fa)
104         t[i].size--;
105     if(x==root)
106         root=son;
107 }
108 int get_rank(int x)
109 {
110     int o=root,ans=0;
111     while(o)
112     {
113         if(t[o].num<x)
114             ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1];
115         else
116             o=t[o].ch[0];
117     }
118     return ans;
119 }
120 int get_kth(int x)
121 {
122     int o=root;
123     while(1)
124     {
125         if(t[t[o].ch[0]].size==x-1)
126             return o;
127         else if(t[t[o].ch[0]].size>=x)
128             o=t[o].ch[0];
129         else
130             x-=t[t[o].ch[0]].size+1,o=t[o].ch[1];
131     }
132     return o;
133 }
134 int get_front(int x)
135 {
136     int o=root,ans=-INF;
137     while(o)
138     {
139         if(t[o].num<x)
140             ans=max(ans,t[o].num),o=t[o].ch[1];
141         else
142             o=t[o].ch[0];
143     }
144     return ans;
145 }
146 int get_behind(int x)
147 {
148     int o=root,ans=INF;
149     while(o)
150     {
151         if(t[o].num>x)
152             ans=min(ans,t[o].num),o=t[o].ch[0];
153         else
154             o=t[o].ch[1];
155     }
156     return ans;
157 }
158 int main()
159 {
160     cnt=2;
161     root=1;
162     t[1].num=-INF,t[1].size=2,t[1].ch[1]=2;
163     t[2].num=INF,t[2].size=1,t[2].fa=1;
164     cin>>n;
165     int tmp,x;
166     for(int i=1;i<=n;i++)
167     {
168         cin>>tmp>>x;
169         if(tmp==1)
170             insert(x);
171         if(tmp==2)
172             erase(get_num(x));
173         if(tmp==3)
174             cout<<get_rank(x)<<endl;
175         if(tmp==4)
176             cout<<t[get_kth(x+1)].num<<endl;
177         if(tmp==5)
178             cout<<get_front(x)<<endl;
179         if(tmp==6)
180             cout<<get_behind(x)<<endl;
181     }
182 }

替罪羊树与同类的平衡树相比,对查询操作具有极大的优势(重量平衡树的特性,当然对于那些维护序列的操作,重量平衡树没有用武之地)

而且实验证明在某种程度上是最快的?其实应该是因题而异

那么这个树的真实应用是啥呢?

替罪羊树可以优化无法旋转的树形结构的时间复杂度,即树套树

第一种情况是与K-D树的嵌套,第二种情况是与线段树的嵌套

平衡树套线段树通常不可写,因为这样嵌套之后平衡树无法旋转,但是用替罪羊树套函数式线段树是没有任何问题的

在介绍树套树部分时,将着重介绍这部分内容

posted @ 2018-07-18 17:33  静听风吟。  阅读(1115)  评论(0编辑  收藏  举报