平衡树 替罪羊树(Scapegoat Tree)

替罪羊树(Scapegoat Tree)

入门模板题 洛谷oj P3369

题目

  您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入xx数
  2. 删除xx数(若有多个相同的数,因只删除一个)
  3. 查询xx数的排名(排名定义为比当前数小的数的个数+1+1。若有多个相同的数,因输出最小的排名)
  4. 查询排名为xx的数
  5. xx的前驱(前驱定义为小于xx,且最大的数)
  6. xx的后继(后继定义为大于xx,且最小的数)

输入格式

  第一行为n,表示操作的个数,下面n行每行有两个数optxopt表示操作的序号( 1opt6 )

输出格式

  对于操作3,4,5,6每行输出一个数,表示对应答案

输入样例

10

1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

输出样例

106465

84185
492737

数据范围

1.n的数据范围: n100000

 

2.每个数的数据范围: [-10^7, 10^7]

 

 

  网上的资料比较琐碎难懂,之前看了很多资料一直不能理解平衡树(我太弱了)……前几天突然莫名其妙明白了,想写一篇笔记记录一下(乱写一通)。

 

0x00 二叉查找树

  要初步弄懂平衡树,首先要知道这是一棵二叉查找树

  二叉查找树(Binary Search Tree),当然也可以叫它二叉搜索树,或者二叉排序树(反正都一个意思都是二叉树),它的定义如下:

    或者是一棵空树,或者是具有下列性质的二叉树:

(1)若左子树不空,则左子树上所有结点的值均小于它的根节点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;

    差不多就像下面这幅图一样:

 

0x01 平衡树的用途

  在学平衡树这个数据结构前,相信我们一定会先有个问题:平衡树能拿来干什么?

  网上的很多资料对这一点写得不太明白(也可能是我太弱了),我先试着乱总结一下:
    现在需要一种数据结构,它需要做到以下几点:
     1. 高效地查询一个序列中某个数的前面和后面的数(a[i-1]和a[i+1])。
     2. 高效地知道第i个数是什么(即a[i])。
     3. 高效地插入和删除。
  显然,我们可以用普通数组优秀地完成第1点和第2点,但第3点不能够了。当然,我们也可以用链表,复杂度O(1)优秀地完成第1点和第3点,但是对于第2点,链表的复杂度就达到了不太理想的O(n)。
  那我们能不能想办法优化一下链表呢?


  我们可以回头看一下二叉搜索树的定义,然后我们会发现,假设存在一个从1到8的链表(就像下图,win自带画图画的,不太好看)

  其实它也满足左边小于右边的定义,也可以勉强算是一棵以序号为每个节点的权值的二叉查找树。

  

 

  但是这样的一棵二叉树是不是很难看很畸形?
  我们怎么想办法把它变成一棵比较好看的树呢?
  可以拿笔画画看:
  我们尝试把中间的节点(4或者5,我选择4)拎出来,然后就变成了这样:

  

  是不是好看了一点?有点树的形状了,那我们可以尝试继续把两侧链上的中间节点继续拎出来,不断重复,最终会变成一棵比较好看的树。

  
  这就是平衡二叉树,严格遵守了二叉查找树的定义——左儿子小于右儿子。
  也许你会有疑问:长成这样的一棵树,怎么做到刚刚链表都不能完全做到的3点要求呢?
  让我们再看看这棵树:

0x02 查询前驱和后驱
  对于一个我们已知的节点
i,我们先定义与它深度相同的都是它的兄弟节点。

  那么很显然,i的左兄弟及其子树上的所有节点都比i的左儿子及其子树上的所有节点来得小,且i的左儿子及其子树上的所有点都比i的父亲更大,所以显然,i的前驱要在i的左儿子及其子树上找。

  同理,仔细观察图,会发现我们所要找的前驱,存在于i的左儿子子树的最右侧,就是i的左儿子的右儿子的右儿子的右儿子的右儿子……(直到最后一个右儿子)

  这样,我们就可以O(log n)地求出i的前驱了。i的后驱同理。

 

0x03 查询第i个数

  前面说过,我们用数的序列编号作为节点的排序权值,所以我们只要像线段树那样从根部开始查一遍就可以了。详细解释:

  从根结点开始,如果第i个数比当前节点j的序号小,就往左儿子搜,反之右儿子。直到找到第i个数,时间复杂度还是O(log n)。


0x04 插入和删除
  插在原序列的末尾,所以新节点的编号是n+1。然后我们把这个节点变成根结点的右儿子的右儿子的右儿子……(变成最后一个没有右儿子的节点的右儿子)。

  删除。其实就是把i节点打个被删除掉的标记(甚至不打也可以)。然后让i的左右儿子之一成为i的父节点的新儿子(就是让某一个儿子取代i节点)。复杂度同O(log n)

  基本操作差不多就是这样。

  那么其实还有一个问题:如果操作太多,导致一棵本来平衡的树变成了一条链表,复杂度爆炸,怎么办呢?

 

0x05 重新建树
  如果你选择的是替罪羊树,那就是优雅的暴力了。替罪羊树在每个节点上记录子树的节点数size,同时还有一个平衡因子alpha(通常在0.5左右,我选择0.7),当每次更新后,递归回去检查i节点的左右儿子分别乘以平衡因子,是否大于另一个儿子,如果大了,代表这棵树有退化的倾向,赶紧拍平重建(就是把树压成链表,重新建树)。

  大概就是这样,手机打的好难受,直接上代码好了。

  (其实我一开始做平衡树时觉得可以用线段树模拟的emmm,就不说了)

  代码可以配合洛谷的模板题食用

 

 

#include <cstdio>
#include <iostream>
using namespace std;
const int INF=1000000000;
const int MAX_N=2000005;
const double alpha=0.75;

int n;
inline int read(){
    register int ch=getchar(),x=0,f=1;
    while (ch<'0'||ch>'9'){
        if (ch=='-')    f=-1;
        ch=getchar();
    } while (ch>='0'&&ch<='9'){
        x=x*10+ch-'0';
        ch=getchar();
    } return x*f;
}
struct Tree{
    int fa;
    int size;
    int value;
    int son[2];
}tree[MAX_N];
int cnt=2;
int root=1;
int node[MAX_N];
int sum;

bool balance(int x){    //判断是否平衡 
    return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size;
}
int build(int l,int r){    //重新递归建树 
    if (l>r)    return 0;
    int mid=(l+r)>>1;
    tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid];
    tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1;
    return node[mid];
}
void recycle(int x){    //把树压成数列 
    if (tree[x].son[0])    recycle(tree[x].son[0]);
    node[++sum]=x;
    if (tree[x].son[1])    recycle(tree[x].son[1]);
}
void rebuild(int x){
    sum=0;
    recycle(x);
    int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum);
    tree[tree[fa].son[son]=now].fa=fa;
    if (x==root)    root=now;
}
void insert(int x){
    int i=root,now=++cnt;    //新节点序号 
    tree[now].size=1,tree[now].value=x;
    while (true){
        tree[i].size++;
        bool son=(x>=tree[i].value); 
        if (tree[i].son[son])    i=tree[i].son[son];
        else{
            tree[tree[i].son[son]=now].fa=i;
            break;
        }
    }
    int flag=0;
    for (int j=now;j;j=tree[j].fa)    //logn找不平衡的节点 
        if (!balance(j))    flag=j;
    if (flag)    rebuild(flag);    //重建树 
}


int get_num(int x){     
    int i=root;
    while (true){
        if(tree[i].value==x)    return i;
        else    i=tree[i].son[tree[i].value<x];
    }
}
void erase(int x){    //删除 
    if (tree[x].son[0]&&tree[x].son[1]){
        int now=tree[x].son[0];
        while (tree[now].son[1])    now=tree[now].son[1];
        tree[x].value=tree[now].value;
        x=now;
    }
    int son=(tree[x].son[0])?tree[x].son[0]:tree[x].son[1];
    int k=(tree[tree[x].fa].son[1]==x);
    tree[tree[tree[x].fa].son[k]=son].fa=tree[x].fa;
    for (int i=tree[x].fa;i;i=tree[i].fa)
        tree[i].size--;
    if (x==root)
        root=son;
}
int get_rank(int x){
    int i=root,ans=0;
    while (i)
        if(tree[i].value<x)    ans+=tree[tree[i].son[0]].size+1,i=tree[i].son[1];
        else    i=tree[i].son[0];
    return ans;
}
int get_kth(int x){
    int i=root;
    while (true)
        if (tree[tree[i].son[0]].size==x-1)    return i;
        else if (tree[tree[i].son[0]].size>=x)    i=tree[i].son[0];
        else    x-=tree[tree[i].son[0]].size+1,i=tree[i].son[1];
    return i;
}
int get_front(int x){
    int i=root,ans=-INF;
    while(i)
        if(tree[i].value<x)    ans=max(ans,tree[i].value),i=tree[i].son[1];
        else    i=tree[i].son[0];
    return ans;
}
int get_behind(int x){
    int i=root,ans=INF;
    while(i)
        if(tree[i].value>x)    ans=min(ans,tree[i].value),i=tree[i].son[0];
        else    i=tree[i].son[1];
    return ans;
}
int main(){
//    freopen("test1.in","r",stdin);
    tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2;
    tree[2].value=INF,tree[2].size=1,tree[2].fa=1;
    n=read();
    for(int i=1,op,x;i<=n;i++){
        op=read(),x=read();
        if(op==1)    insert(x);
        if(op==2)    erase(get_num(x));
        if(op==3)    printf("%d\n",get_rank(x));
        if(op==4)    printf("%d\n",tree[get_kth(x+1)].value);
        if(op==5)    printf("%d\n",get_front(x));
        if(op==6)    printf("%d\n",get_behind(x));
    }
}

 

 

 

 

 

 

其他例题

大体上按难度排序?我太弱了也搞不懂。

一.[HNOI2002]营业额统计 (2019.4.10更新)

洛谷oj P2234

  据说有各种神犇用许多神奇的解法A掉了……但是我这种蒟蒻就先用平衡树练手了。

  min{|该天以前某一天的营业额-该天营业额|},即不大于a[i]的最大值和不小于a[i]的最小值,就是寻找a[i]的前驱和后驱,分别减去a[i]取绝对值,再全部加起来,复杂度应该是O(nlogn)。

  代码:

#include <cstdio>
#include <iostream>
using namespace std;
const int INF=1000000000;
const int MAX_N=2000005;
const double alpha=0.75;

int n;
inline int read(){
    register int ch=getchar(),x=0,f=1;
    while (ch<'0'||ch>'9'){
        if (ch=='-')    f=-1;
        ch=getchar();
    } while (ch>='0'&&ch<='9'){
        x=x*10+ch-'0';
        ch=getchar();
    } return x*f;
}
struct Tree{
    int fa;
    int size;
    int value;
    int son[2];
}tree[MAX_N];
int cnt=2;
int root=1;
int node[MAX_N];
int sum;

bool balance(register int x){    //判断是否平衡 
    return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size;
}
inline int build(register int l,register int r){    //重新递归建树 
    if (l>r)    return 0;
    int mid=(l+r)>>1;
    tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid];
    tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1;
    return node[mid];
}
void recycle(register int x){    //把树压成数列 
    if (tree[x].son[0])    recycle(tree[x].son[0]);
    node[++sum]=x;
    if (tree[x].son[1])    recycle(tree[x].son[1]);
}
void rebuild(register int x){
    sum=0;
    recycle(x);
    int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum);
    tree[tree[fa].son[son]=now].fa=fa;
    if (x==root)    root=now;
}
void insert(register int x){
    int i=root,now=++cnt;    //新节点序号 
    tree[now].size=1,tree[now].value=x;
    while (true){
        tree[i].size++;
        bool son=(x>=tree[i].value); 
        if (tree[i].son[son])    i=tree[i].son[son];
        else{
            tree[tree[i].son[son]=now].fa=i;
            break;
        }
    }
    int flag=0;
    for (int j=now;j;j=tree[j].fa)    //logn找不平衡的节点 
        if (!balance(j))    flag=j;
    if (flag)    rebuild(flag);    //重建树 
}

inline int get_front(register int x){
    register int i=root,ans=-INF;
    while(i)
        if(tree[i].value<x)    ans=max(ans,tree[i].value),i=tree[i].son[1];
        else    i=tree[i].son[0];
    return ans;
}
inline int get_behind(register int x){
    register int i=root,ans=INF;
    while(i)
        if(tree[i].value>x)    ans=min(ans,tree[i].value),i=tree[i].son[0];
        else    i=tree[i].son[1];
    return ans;
}
bool flag[1000000+5];
int result;
int main(){
//  freopen("test1.in","r",stdin);
    tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2;
    tree[2].value=INF,tree[2].size=1,tree[2].fa=1;
    n=read();
    int kkk=read();
    result+=kkk;
    insert(kkk);
    flag[kkk]=true;
    for (int i=2,a,min,max;i<=n;i++){
        a=read();
        if (flag[a])    continue;
        flag[a]=true;
        insert(a);
        min=get_front(a);
        max=get_behind(a);
        if (min==-INF){
            result+=(max-a);
            continue;
        }
        else if (max==INF){
            result+=(a-min);
            continue;
        }
        result+=(a-min>max-a?max-a:a-min);
    }
    printf("%d",result);
    return 0;
}

 

 

 

 

[学习自百度百科和其他网络资料]

转载于:https://www.cnblogs.com/awakening-orz/p/10660378.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值