Loading

左偏树

左偏树,是一种可以在 \(\Theta(\log_2 n)\) 复杂度内合并的一种数据结构。

它是以二叉树的形态来维护堆的性质,由此也延伸出很多别的操作。

接下来本文所述堆及其操作均默认为小根堆。

\[\]

一些约定

  • \(v_i\) 节点 \(i\) 的权值。

  • \(l_i,r_i\) 节点 \(i\) 的左右儿子。

  • \(d_i\) 节点 \(i\) 到其最近外节点的距离。

  • \(f_i\) 节点 \(i\) 的父节点。

什么是外节点:一个左儿子或右儿子是空节点的节点。

\[\]

左偏树的性质

堆性质

作为堆式数据结构显然有堆的性质,即对任意一个树中的点 \(i\)\(v_i<v_{l_i},v_i<v_{r_i}\)

但是任意一个节点左右儿子的大小并不确定,这也不在我们的维护范围之内。

左偏性质

左偏,就是对于任意一个树中的点 \(i\)\(d_{l_i}>d_{r_i}\)

另外的,对于任意一个树中的点 \(i\)\(d_i=d_{r_i}+1\)

\[\]

左偏树的操作

合并操作

左偏树最核心的操作,其余的操作也均以这个操作为基础。

假设我们要合并分别以 \(a,b\) 为根节点的两棵左偏树。

首先,若其中一棵树的根为空节点,则另一棵树的根成为新树的根。

若两个树根均存在,那么权值更小的必定成为新的树根。

这里我们假设 \(v_a<v_b\),即 \(a\) 为新树的根。为了避免分类讨论,当 \(v_a>v_b\) 时,我们可以将其交换。

此时我们再令 \(b\)\(a\) 的右儿子进行合并,重新比较其大小。如此不断比较下去直到遇到空节点或叶节点为止。

这样递归返回每次合并的根节点的值,重置此树的相关节点。

另外,由于是对右儿子进行合并,所以可能合并后的树不再满足左偏性质。此时我们交换左右子树。

我们要时刻保证对于任意一个树中的节点 \(i\)\(d_i=d_{r_i}+1\),所以我们重新规定一下右儿子的距离即可。

实现也比较容易。

int merge(int x,int y){
  if(!x||!y) return x+y;
  if(v[y]<v[x]) swap(x,y);
  rs=merge(rs,y);
  if(dis[ls]<dis[rs])swap(ls,rs);
  dis[x]=dis[rs]+1;
  return x;
}

还有一种写法是随机合并,引入随机数来判断是否交换左右节点。

这样做可能会使树不满足左偏性质,但是省去了距离的相关计算,且复杂度不变。

取最值操作

由于左偏树的堆性质,它的最值肯定在堆顶。那么就直接不断跳父节点就行了。

但是我们发现暴跳太慢了。我们可以借助并查集对其路径压缩。

对于同一棵左偏树中的点用并查集连接到一起,并钦定其祖先节点为堆顶。

于是就可以在 \(\Theta(\log_2 n)\) 的时间内快速找出最值了。

删除操作

删除操作只针对堆顶节点来说。

删除节点需要重置其信息(左右儿子和距离)。

另外,删除此节点之后,其真正的左右儿子并没有被删除,但是现在他们都没有了父亲(?),所以我们需要合并这两个点。

fa[lson[x]]=fa[rson[x]]=fa[x]=merge(lson[x],rson[x]);

注意这个节点本身的父亲也要指向合并后的根节点。

因为路径压缩后删除此节点,此节点的父亲指向的是自己,若不更改可能会导致后面出各种问题。

插入节点

等同于将其与一棵大小为一的堆合并。

此时要保证插入的这个点的各个信息已经处理完善。

全树加/减/乘一个值

维护懒标记,从根节点开始不断向下穿即可。具体是在删除/合并访问儿子时下传。

由于只能单个传,所以速度较慢。

当然不止这三种操作,可以打标记且不改变相对大小的操作都可以。

\[\]

例题

Monkey King

给定初始的 \(n\) 个大小为一的堆及其权值,再给定 \(m\) 次询问。
每次询问 x y,表示将 \(x\)\(y\) 所在的堆的堆顶权值变为原来的一半后合并。
对于每次操作,输出新堆的堆顶权值。
\(n,m\le 100000\)

一开始的思路是,每次新建两个权值分别为两个堆顶权值一半的点,先将这两个点与这两个堆合并,再删除堆顶,最后合并两个堆。

至于权值方面,由于我懒得写大根堆,于是就将权值的相反数插入堆中建小根堆。

注意奇数的整除是下取整,所以对于奇数 \(a\),要将 a>>=1 写成 a=-((-a)>>1)

计算下来若每次新建两个点,那么总共就是 \(2\times m\) 个,加上原来的应该不超过 \(300000\) 个点。

但不知道为什么总是 MLE,至今没找到原因(

所以我换了写法,不再新建节点,而是先删除两个堆顶,再在每个堆中加入新点,只不过点的编号还是原来堆顶的。

最后合并两个堆就做完了。

代码和板子的差不多。

namespace LIT{
  #define ls lson[x]
  #define rs rson[x]
  int fa[maxn],dis[maxn];
  int lson[maxn],rson[maxn];
  
  struct node{
    int pos,val;
    bool operator < (const node &b) const{
      return val^b.val?val<b.val:pos<b.pos;  
    } 
  }v[maxn];
  
  int findf(int x){
    return fa[x]==x?x:fa[x]=findf(fa[x]);
  }
  
  int merge(int x,int y){
    if(!x||!y) return x+y;
    if(v[y]<v[x]) swap(x,y);
    rs=merge(rs,y);
    if(dis[ls]<dis[rs])swap(ls,rs);
    dis[x]=dis[rs]+1;
    return x;
  }
}

using namespace LIT;

signed main(){
  dis[0]=-1;
  while(scanf("%d",&n)!=EOF){
    for(int i=1;i<=n;i++){
      lson[i]=rson[i]=dis[i]=0;
      v[i].val=read()*-1;
      v[i].pos=fa[i]=i;
    }
    m=read();cnt=n;
    for(int i=1,x,y;i<=m;i++){
      x=findf(read());y=findf(read());
      if(x==y){printf("-1\n");continue;}
      v[x].val=-((-v[x].val)>>1);
      int rt1=merge(lson[x],rson[x]);
      lson[x]=rson[x]=dis[x]=0;
      int now1=fa[rt1]=fa[x]=merge(rt1,x);
      v[y].val=-((-v[y].val)>>1);
      int rt2=merge(lson[y],rson[y]);
      lson[y]=rson[y]=dis[y]=0;
      int now2=fa[rt2]=fa[y]=merge(rt2,y);
      fa[now1]=fa[now2]=merge(now1,now2);
      printf("%d\n",-v[fa[now1]].val);
    }
  }
  return 0;
}
posted @ 2021-06-25 20:57  KnightL  阅读(195)  评论(0编辑  收藏  举报