zoukankan      html  css  js  c++  java
  • 左偏树入门

    一、左偏树能做什么?

    左偏树(Leftist Tree)是一种维护可并堆(Mergeable Heap)的数据结构。

    可并堆是一种抽象数据结构(Abstract Data Type, ADT),在普通的堆(Heap)的基础上,增添了 upd 操作,使得两个堆可以合并。这也是其名之来历。

    二、左偏树的基本操作

    左偏树的基本操作有三种:

    1. 一切操作的核心:merge 操作(合并)
    2. del 操作【删除操作,分为删除堆顶元素(入门)和删除任意元素(进阶)】
    3. add 操作(加点操作)

    本文将以洛谷的左偏树模板 P3377 为纲,故不会介绍 del 操作中的删除任意元素和 add 操作,同时,以下的堆实质上都为小根堆。

    三、详解左偏树操作

    • Part ZERO: 有关左偏树的定义与基本性质、定理

      左偏树是一颗二叉树(Binary Tree),它的节点除了和二叉树的节点一样具有左右子树(Left Subtree, ls; Right Subtree, rs)以外,还有两个属性:键值(Value, v)距离(Distance, dis)

      • Definition ONE: 外节点(External Node)

        节点 (i) 被称为外节点,当且仅当节点 (i) 的左子树或右子树为空。

      • Definition TWO: 距离

        节点 (i)距离是它到其后代中最近的外节点所经过的边数。

        特别地,如果节点 (i) 本身就是外节点,则其距离为 (0); 空节点的距离被规定为 (-1).

        通常称一棵左偏树的距离为其根节点的距离。

      • Property ONE: 任意节点的键值不大于它的左右子节点的键值(小根堆性质)。

      Property ONE 可得:

      • Theorem ONE: 左偏树的根节点的键值是所有节点中最小的。

      Theorem ONE, 我们可以用 (O(1)) 的时间复杂度得到左偏树中最小的节点。

      • Property TWO: 任意节点的左子节点的距离不小于其右子节点的距离(左偏性质)。

      小根堆性质与左偏性质对于每一个节点都成立。因此,显然可以得出,左偏树的左右子树皆为左偏树。同时,我们可以得出左偏树之定义:具有左偏性质的堆有序二叉树是左偏树

      Property TWO 可知:

      • Property THREE: 任意节点的距离等于其右子节点的距离加 (1).

      接下来,我们将要讨论左偏树的距离与节点数之间的关系。

      • Lemma ONE: 若一棵左偏树的距离为一定值,则在所有可能的左偏树当中,节点最少的是一棵完全二叉树。

        Proof: 由 Property TWO 可证。

      Lemma ONE 可知:

      • Theorem TWO: 若一棵左偏树的距离为 (k), 则这颗左偏树至少有 ((2^{k+1}-1)) 个节点。

      由此推论可知:

      • Corollary ONE: 一棵 (n) 个节点的左偏树的距离最大值为 (log_2(n+1)-1).
    • Part ONE: merge 操作

      merge 操作在代码中体现为 int merge(int x,int y) 函数。其中,令 (x)(A) 堆的堆顶,(y)(B) 堆的堆顶。merge 操作的任务即是将 (B) 堆与 (A) 堆的右子树合并,且维护左偏树的性质。

      merge 操作分为以下几个过程:

      Step ONE: 特判。如果两个合并的堆有任意一个为空,则返回另一个堆。

      Step TWO: 判断优先级。判断两堆堆顶的键值大小。如果 (x) 的键值大于 (y) 的键值,则说明如果仍然将当前的 (B) 堆与 (A) 堆的右子树合并,就无法维护左偏树的小根堆性质,故需要 swap(x,y), 即将 (A) 堆和 (B) 堆“交换”。

      Step THREE: 合并。将 (B) 堆与 (A) 堆的右子树合并。

      Step FOUR: 判断距离。如果 (A) 的右子树的距离比左子树大,则违背了左偏树的左偏性质。则进行交换。

      Step FIVE: 更新距离。将整棵左偏树的距离进行更新。

      代码如下:

      const int maxn=100005;
      struct Node{
      	int ls;
      	int rs;
      	int v;
      	int dis;
      	int id;
      }tr[maxn];
      
      int merge(int x,int y){
      	if(!x||!y)return x+y;//Step ONE
      	if(tr[x].v>tr[y].v)
      		swap(x,y);//Step TWO
      	tr[x].rs=merge(tr[x].rs,y);fa[tr[x].rs]=x;//Step THREE
      	if(tr[tr[x].rs].dis>tr[tr[x].ls].dis)
      		swap(tr[x].ls,tr[x].rs);//Step FOUR
      	tr[x].dis=tr[x].rs?(tr[tr[x].rs].dis+1):0;//Step FIVE
      	return x;
      }
      

    Part TWO: del 操作

    del 操作相对来说非常容易理解,它所要做的就是删除一个堆的堆顶,并重新整理其他的节点使其重新成为一个堆。

    删除堆顶很容易,我们只需要将堆顶节点的信息全部清空即可。

    重新整理呢?我们换个角度想想。其实,我们只需要将原堆顶节点的两个子树使用 merge 操作合并!

    代码如下:

    int del(int x){
    	int fx=tr[x].ls;int fy=tr[x].rs;
    	tr[x].ls=0;tr[x].rs=0;
    	tr[x].dis=0;
    	fa[fx]=fx;fa[fy]=fy;
    	int tmp=merge(fx,fy);
    	return fa[x]=tmp;
    }
    

    Part THREE: 全部代码

    下面的代码是洛谷 P3377 【模板】左偏树(可并堆)的 AC 代码。由于题目的要求,可能部分代码与上面的解释会有所不同,但不影响理解。

    #include<bits/stdc++.h>
    
    using namespace std;
    
    void rd(int &x){
    	x=0;int f=1;char ch=getchar();
    	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
    	x*=f;
    }
    
    const int maxn=100005;
    struct Node{
    	int ls;
    	int rs;
    	int v;
    	int dis;
    	int id;
    }tr[maxn];
    int fa[maxn];
    bool exist[maxn];
    int n,m;
    
    int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
    
    int merge(int x,int y){
    	if(!x||!y)return x+y;
    	if(tr[x].v>tr[y].v||(tr[x].v==tr[y].v&&tr[x].id>tr[y].id))
    		swap(x,y);
    	tr[x].rs=merge(tr[x].rs,y);fa[tr[x].rs]=x;
    	if(tr[tr[x].rs].dis>tr[tr[x].ls].dis)
    		swap(tr[x].ls,tr[x].rs);
    	tr[x].dis=tr[x].rs?(tr[tr[x].rs].dis+1):0;
    	return x;
    }
    
    int del(int x){
    	int fx=tr[x].ls;int fy=tr[x].rs;
    	tr[x].ls=0;tr[x].rs=0;
    	tr[x].dis=0;
    	fa[fx]=fx;fa[fy]=fy;
    	int tmp=merge(fx,fy);
    	return fa[x]=tmp;
    }
    
    int main(){
    	rd(n);rd(m);
    	fill(exist+1,exist+n+1,true);
    	for(int i=1;i<=n;i++){
    		rd(tr[i].v);
    		tr[i].id=fa[i]=i;
    	}
    	for(int i=1;i<=m;i++){
    		int op,x,y;
    		rd(op);rd(x);
    		if(op==1){
    			rd(y);
    			if(!exist[x]||!exist[y])continue;
    			int fx=find(x);int fy=find(y);
    			if(fx!=fy)merge(fx,fy);
    		}else if(op==2){
    			if(!exist[x]){cout<<-1<<endl;continue;}
    			x=find(x);
    			cout<<tr[x].v<<endl;
    			del(x);
    			exist[tr[x].id]=false;
    		}
    	}
      return 0;
    }
    
    转载是允许的,但是除了博主同意的情况下,必须在文章的明显区域说明出处,否则将会追究其法律责任。
  • 相关阅读:
    改进的延时函数Delay(使用MsgWaitForMultipleObjects等待消息或超时的到来)
    罗斯福新政
    保存网页为图片——滚动截取IE(WebBrowse)
    Linux LVM硬盘管理及LVM分区扩容
    visual leak dector内存泄漏检测方法
    小智慧30
    函数调用的原理
    HTTP协议漫谈
    Boost源码剖析之:泛型指针类any
    扩展C++ string类
  • 原文地址:https://www.cnblogs.com/Xray-luogu/p/14532738.html
Copyright © 2011-2022 走看看