Hile每日算法-4.23-左偏树

左偏树

咕咕咕

好久没写博客了,之前坚持三天就鸽了证明自己一个月啥都没学,以后还是要写的。

以下内容参考了大佬的博客luoguP3377的题解区,%%%。

堆,这个肯定都知道。“不就是优先队列吗”,本来一直保持着这样的想法,直到前几天帮室友验一道给数据结构基础课出的题时,突然发现自己连个堆都实现不来(这就是不听课的后果)有一说一真的菜b,于是为了偷懒(?学了一个神奇的数据结构——左偏树。

首先,我们知道堆的两个性质:

[1].堆是一棵完全二叉树

[2].堆上各结点的值从根往下具有单调性(此处指非严格单调性即可以相等,下同)

由此,我们可以得到一些结论:

[3].设结点数量为nn的堆的最大深度dd,由[1]得2d2log2n+12^d\le2^{log_2n+1},深度最大为log2n+1log_2n+1

[4].由[2]&[3]得,要将根节点调整为最值,最多从根开始遍历一整条链。因此往堆中增删根结点,调整堆的时间复杂度是O(logn)O(logn)

所以,我们就获得了一个O(logn)O(logn)时间动态求最值的数据结构。

但是,更进一步,如果要求合并两个大顶堆,该怎么做呢?

这题我会暴力啊 显然,如果操作数和nn差不多的话,暴力就会导致O(n2logn)O(n^2logn)的整体复杂度,这时就应该换个数据结构了。

左偏树(Leftist Tree/Leftist Heap)

在说左偏树之前,我们需要定义一个变量disidis_i,其定义为ii结点和最近空结点的距离1-1,当ii为空结点时disi=1dis_i=-1,正是由于disdis的存在,我们才能保证左偏树的效率。

首先,仿照堆,我们也说说左偏树的性质:

[1].对于左偏树上的所有结点,都存在dislsdisrsdis_{ls}\ge dis_{rs},其中ls,rsls,rs分别指当前点的左右子结点

[2].左偏树上各结点的值和disdis从根往下具有单调性

类比上面对堆的描述,我们也可以得到一些结论:

[3].由[1]&[2]得,对于每一个结点总存在disi=disrs+1dis_i=dis_{rs}+1,其中rsrsii的右孩子。

[4].设结点数量为nn的左偏树的**根节点的disdis**为dd,由[1]&[3]得2d+1n+12^{d+1}\le n+1dd最大为log2(n+1)1log_2(n+1)-1

在上面的性质和结论的基础上,我们就可以进行核心的合并操作了:

设有两棵根节点为最小值的左偏树aabbviv_i表示ii结点的值,rir_i表示ii结点的右孩子,假设vavbv_a\le v_b

由[3]&[4],我们可以知道一棵nn个结点的左偏树最多有lognlogn条的从根一直往右的边(下称为右向边),从而可以在O(logn)O(logn)的时间内在右向边上找到一个vavivbvriv_a\le v_i\le v_b\le v_{r_i},所以可以不影响左偏性地将bb插入到iirir_i之间,随后从bb开始往aa更新disdis,同时判断是否存在不满足[3]的情况,若存在则交换当前结点的左右子树。

于是就愉快地结束啦!什么?你问怎么删除堆顶?

没问题呀,左偏树的子树都具有左偏性质,去掉堆顶就相当于把原来的根的两棵子树重新合并,直接把之前的根重置后merge就好了。

img

(图片来源:洛谷)

那就趁热来一……道例题吧!

hdu1512 Monkey King

题意大致是,初始有nn个互不认识的猴子,每个猴子都有一个战斗力,他们会打mm场架,每场架的双方为aabb,双方会找各自的朋友中战斗力最高的那个(可能是他自己)来打架,之后双方打了架的猴子元气大伤战力减半,然后两方猴成为了朋友(不打不相识?,对于每个aabb,若双方已经为朋友则输出1-1,否则输出打完架之后,这一帮猴子中最高的战斗力。

这就是左偏树模板题了(虽然二项堆/斐波那契堆/配对堆好像也能做,不过我太菜了还不会),初始有nn个大顶堆,每次打架取出两堆顶,减半后放入原堆并把两个堆合二为一并用并查集维护朋友关系。左偏树写起来就很简单,来回merge一下就ac啦,单组数据时间复杂度O(mlogn)O(mlogn)

AC代码:

#include<bits/stdc++.h>
#define N 100010
using namespace std;
int n;
struct LeftTree
{
    int d,v,l,r,f;//分别代表dis,value,leftson,rightson,father
}lt[N];
void init(int n)//多组数据初始化
{
    for(int i=1;i<=n;i++)lt[i].f=i,lt[i].l=lt[i].r=lt[i].v=lt[i].d=0;
}
int find(int x)//并查集
{
    return (x^lt[x].f)?lt[x].f=find(lt[x].f):x;
}
int merge(int x,int y,int rt=0)//返回新堆的根
{
    if(!x||!y){lt[x+y].f=rt?rt:x+y;return x+y;}
    if(lt[x].v<lt[y].v)swap(x,y);//大顶堆
    lt[x].f=rt?rt:rt=x;//更新父结点(根)
    lt[x].r=merge(lt[x].r,y,rt);//递归右子树
    if(lt[lt[x].l].d<lt[lt[x].r].d)swap(lt[x].l,lt[x].r);//调整左右结点dis
    lt[x].d=lt[lt[x].r].d+1;
    return x;
}
int m,x,y;
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n)
    {
        init(n);
        for(int i=1;i<=n;i++)cin>>lt[i].v;
        cin>>m;
        while(m--)
        {
            cin>>x>>y;
            int a=find(x),b=find(y),c,d,e;
            if(a==b)
            {
                cout<<-1<<"\n";
                continue;
            }
            lt[a].v>>=1;lt[b].v>>=1;
            //注意取出堆顶之后要将左右孩子置空,不然会出现两结点互为父亲的情况
            c=merge(lt[a].l,lt[a].r);lt[a].l=lt[a].r=0;
            d=merge(lt[b].l,lt[b].r);lt[b].l=lt[b].r=0;
            a=merge(a,c);b=merge(b,d);//放回减半的结点
            e=merge(a,b);//合并两棵树
            cout<<lt[e].v<<'\n';
        }
    }
}

P.S:一直有个疑问,树上的“结点”和“节点”应该用哪个,还是说哪个都可以吗直接说node不香吗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章