2021-05-07

左偏树

可并堆
1.堆是非常常用的数据结构,C++的STL为我们提供了方便好用的priority_queue,节省了我们大量的时间。但是,priority_queue因为过度封装以及采用的数据结构为二叉堆,有许多缺点,其中一个就是:无法高效的合并两个堆。每一次把size小的堆的点暴力插入到size大的堆里面,这个方法十分不优秀,时间复杂度是O(nlog2n)
2.如果我们要高效的合并两个堆就需要其他特殊的堆来支持高效合并,这种堆就叫做可并堆。
何为左偏树
左偏树(英语:leftist tree或leftist heap),也可称为左偏堆、左倾堆,是计算机科学中的一种树,是一种优先队列实现方式,属于可并堆,是最常见的可并堆,在信息学中十分常见,在统计问题、最值问题、模拟问题和贪心问题等等类型的题目中,左偏树都有着广泛的应用。斜堆是比左偏树更为一般的数据结构。
左偏树顾名思义就是偏向左面的树,如左图就是一棵左偏树,这棵树明显满足一个性质。我们定义dis[p]为从p节点出发可以向右走的最大步数,从右图中可以看到,当前点的左儿子的dis值都比右儿子的dis值大,只要满足这个性质就是左偏树。
在这里插入图片描述在这里插入图片描述
左偏树具体定义
相关定义:
外节点:只有一个儿子或没有儿子的节点,即左右儿子至少有一个为空节点的节点
距离:一个节点到离它最近的外节点的距离,即两节点之间路径的权值和。特别地,外节点的距离为0,空节点的距离为−1
左偏树:一种满足左偏性质的堆有序二叉树(左偏树的左偏性质体现在左儿子的距离大于右儿子的距离)
左偏树的距离:我们将一棵左偏树根节点的距离作为该树的距离

性质:
满足堆的基本性质
对于任意节点,左儿子的距离大于右儿子的距离
对于任意节点,其距离等于它的右儿子的距离加一
对于一棵n个节点的左偏树,其根节点的距离不超过logN
对于一棵距离为d的左偏树,其节点数不少于2k+1−1

节点信息:
左偏树一般存储以下几个节点信息
val:权值
lson:左儿子
rson:右儿子
dist:距离
father:父亲
左偏树基础操作
1.merge
合并是左偏树最重要的操作,毕竟可并堆可并堆,肯定是要能够合并的。
定义一个函数Merge(x,y)表示合并x,y,返回值为合并后的根节点。具体实现流程如下:
设x是x,y中权值较小的一个,即 v a l x ≤ v a l y ( 若 x 的 权 值 大 于 y , 交 换 x , y 即 可 ) 。 递 归 地 向 下 合 并 , 在 x 的 右 子 树 最 右 链 中 找 到 第 一 个 权 值 大 于 y 的 权 值 的 节 点 k , 将 y 作 为 k val_x\leq val_y(若x的权值大于y,交换x,y即可)。递归地向下合并,在x的右子树最右链中找到第一个权值大于y的权值的节点k,将y作为k valxvalyxyx,yxykyk的父亲。
(若x的权值大于y,交换x,y即可)。递归地向下合并,在x的右子树最右链中找到第一个权值大于y的权值的节点k,将y作为k的父亲。
2.继续递归,合并y的右子树和k的右子树,直到x或y为空。
3.在合并的过程中注意维护左偏性质,即若左儿子的距离小于右儿子的距离,则交换左右儿子。

int Merge(int x,int y)
{
    
    
    if(!x || !y)
        return x+y;
    if(v(x)>v(y) ||(v(x)==v(y) && x>y))
        swap(x,y);
    int &ls=l(x),&rs=r(x);
    rs=Merge(rs,y);
    f(rs)=x;
    if(d(ls)<d(rs))
        swap(ls,rs);
    d(x)=d(rs)+1;
    return x;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
复杂度分析
显然,合并操作与左孩子无关。那么对于总孩子数相同的左孩子越多,效率必然越高;左孩子越少,效率必然越低。那么不妨假设左孩子数的两个极端:
每个左孩子比右孩子恰好多1,两颗树规模为n,m,极端下,两颗树交替成为操作中的A数,有T(n,m) = T(m,n/2) + O(1) = O(lgn + lgm)。
全部为左孩子,为基本情况,复杂度为O(1)。
2.删除根节点
只要先删除根节点,即将根节点的权值赋为−1(其实有的时候不改权值也没影响),然后合并根节点的左右子树就可以了。
删除根节点代码:

void Delroot(int x)
{
    
    
    int ls=l(x),rs=r(x);
    v(x)=-1,f(ls)=0,f(rs)=0;
    Merge(ls,rs);
}

3.删除任意节点
这里的任意节点指的是任意编号的节点而不是任意权值的节点,一般的可并堆是不支持删除给定权值节点的操作的。
与删除根节点类似,先将要删除的节点的权值赋值为−1,然后合并它的左右子树,将合并后新的左偏树接到被删除节点的父节点上就可以了。但是与删除根节点不同的是,这个操作可能会导致整棵左偏树的左偏性质被破坏,因此要从该节点一直向上检查左偏性质,直到左偏性质没有被破坏或者到达了根节点。
删除节点代码:

void Delete(int x)
{
    
    
    int fx=f(x),p=Merge(l(x),r(x));
    int &ls=l(fx),&rs=r(fx);
    f(p)=fx;
    ls==x?ls=p:rs=p;
    while(p)
    {
    
    
        if(d(ls)<d(rs))
            swap(ls,rs);
        if(d(fx)==d(rs)+1)
            return ;
        d(fx)=d(rs)+1;
        p=fx,fx=f(x);
        ls=l(fx),rs=r(fx);
    }
}

4.建树
暴力加点合并的话时间复杂度是O(nlogn),令人难以接受,因此我们需要一个比较高效的方法来实现建树。
建树有以下几个步骤:

  1. 建立一个队列,将每个节点看作一个节点数为1的左偏树加入队列。
  2. 每次取出队头的两棵左偏树,将它们合并,并将合并后的新左偏树加入队列。
  3. 重复第2步,直到队列为空。
    建树代码:
void Build()
{
    
    
    queue<int> q;
    for(int i=1;i<=n;i++)
        q.push(i);
    int x,y;
    while(q.size())
    {
    
    
        x=q.front();q.pop();
        y=q.front();q.pop();
        q.push(Merge(x,y));
    }
}

洛谷P3377

【模板】左偏树(可并堆)
(https://www.luogu.com.cn/problem/P3377)
在这里插入图片描述
由我们的大佬逸凡同学给出题解

#include <bits/stdc++.h>
using namespace std;
template <typename IntOrLong>
inline void read(IntOrLong &NUM)
{
    
    
	IntOrLong num=0;
	bool b=false;
	int c=getchar();
	while ((c>'9' || c<'0') && c!='-') c=getchar();
	if (c=='-') {
    
    b=true; c=getchar();}
	while (c>='0' && c<='9')
	{
    
    
		num=(num<<1)+(num<<3)+c-'0';
		c=getchar();
	}
	NUM=b?0-num:num;
}
const int MAXN=100010;
namespace LeftistTree
{
    
    
    int ls[MAXN],rs[MAXN],dis[MAXN];
    int sfa[MAXN];  //use sfa to get root
    int val[MAXN];

    void push_up(int x)
    {
    
    
        if (dis[ls[x]]<dis[rs[x]]) swap(ls[x],rs[x]);
        dis[x]=dis[rs[x]]+1;
    }
    int merge(int x,int y)
    {
    
    
        if (!x || !y) return x^y;
        if (val[x]>val[y] || (val[x]==val[y] && x>y)) swap(x,y);
        rs[x]=merge(rs[x],y);
        push_up(x);
        return x;
    }
    int getSfa(int x) {
    
    return sfa[x]==x?x:sfa[x]=getSfa(sfa[x]);}    //union-find disjoint sets
    int uni(int x,int y)    //return the root after union
    {
    
    
        if (val[x]==-1 || val[y]==-1) return -1;
        if (!x || !y) return x^y;
        x=getSfa(x); y=getSfa(y);
        if (x==y) return x;
        int z=merge(x,y);
        return sfa[x]=sfa[y]=sfa[z]=z;
    }
    int pop(int x)  //return the value of the root of the pile where x at
    {
    
    
        if (!x || val[x]==-1) return -1;
        x=getSfa(x);
        int y=merge(ls[x],rs[x]);
        sfa[x]=sfa[y]=y;
        int top_value=val[x];
        val[x]=-1;  //delete x, and let nonexistent x has the value of -1
        return top_value;
    }
}
using namespace LeftistTree;
int main()
{
    
    
    int n,m;
    read(n); read(m);
    for (int i=1;i<=n;++i) {
    
    read(val[i]); sfa[i]=i;}
    for (int i=1;i<=m;++i)
    {
    
    
        int ope,x,y;
        read(ope);
        if (ope==1)
        {
    
    
            read(x); read(y);
            uni(x,y);
        }
        else
        {
    
    
            read(x);
            printf("%d\n",pop(x));
        }
    }
    return 0;
}

成功AC

猜你喜欢

转载自blog.csdn.net/ShakingSH/article/details/116479632