Evanyou Blog 彩带

  前言

  之前我一直都这么以为:$Fibonacci$堆虽然快,但是代码复杂度反人类;二项堆打起来麻烦的死,而且又难理解又难调;综合起来还是左偏树最好!!!

  直到我知道了配对堆这个东西。。。。。。

  我$HolseLee$就是省选爆零,暴力分都拿不到,也绝不会学左偏树以外的可并堆!呵呵,配对堆真快,爆了左偏树几条街呢。

  配对堆

  以下部分内容及图片引用自:

  https://www.cnblogs.com/Paul-Guderian/p/7664366.html

  定义

  配对堆($Pairing Heap$)也是属于可并堆的一种,是一棵多叉树。也可以说它是$Fibonacci$堆的简化版本,和$Fibonacci$堆一样,其操作复杂度及其优秀,而且代码复杂度又非常低,与左偏树基本一致,是$OI$比赛的好选择。

  基本操作

  合并($merge$):$O(1)$

  插入新节点($push$):$O(1)$

  修改值($change$):$O(1)/O(log n)$

  取出堆顶元素($top$):$O(1)$

  删除堆顶元素($pop$):$O(log n)$

  

  合并

  可并堆最重要的操作,直接比较两个需要合并的堆的堆顶然后按照大小关系连边就行了。复杂度低到惊人的$O(1)$;

  插入新节点

  可以把新节点看作一个只有一个元素的堆,再与当前堆合并就行了。

  取出堆顶元素

  直接返回堆顶值即可。

  

  修改值

  如何修改一个点的值呢?首先将这个点与父节点之间的连边断开,即令$fa[u]=0$,(如果用邻接链表存边不方便断边就可以不断,因为除了连边以外,判断父子关系还需要子节点的$fa[]$指向父节点,因此不断边实际上也不会有问题)然后修改值即可。

  断掉与父亲的连边后,相当于形成两个堆,接下来进行一次$merge$操作就好了。可以发现这个操作的时间复杂度是$O(1)$,但有资料认为这个操作可能会破坏配对堆应有的结构(这"应有"的结构在下文会体现出来,它是$pop$操作复杂度是$O(log n)$而不是$O(n)$的重要保证),结构改变后就会影响$pop$的复杂度,使其向$O(n)$退化,因此计算后认定其实修改操作从时间复杂度贡献分析来看,可能是$O(log n)$而不是$O(1)$。

  不过反正这个操作比较少用就是了。

  

  删除堆顶元素

  可以发现,以上的操作基本上就像蛇皮怪一样的瞎$merge$连个边就了事了,所以复杂度就全部承受在这个操作上了(爱谴责人士表示强烈$pop$)

  现在需要删除根节点,如果直接把根节点删掉然后把它的子节点一个个连起来的话,可能会使新的根节点有多个子节点然后影响后面的复杂度,使得后面$pop$操作复杂度退化成$O(n)$。较优秀的方法应该是将子节点两两$merge$,直到全部合并在一起为止。这样可以保证后面的复杂度不会退化。

  

  

  模板代码

  照着网上的大佬自己瞎几把打了一份,打的不好看将就着看吧。

  Code:

  

//It is made by HosleLee on 25th Oct 2018
//Pairing_Heap
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+7;
int n,m,val[N],head[N],cnte,fa[N];
struct Edge { int to,nxt; }e[N<<1];
queue<int>q;

inline int read()
{
    char ch=getchar(); int x=0; bool flag=false;
    while( ch<'0' || ch>'9' ) {
        if( ch=='-' ) flag=true; ch=getchar();
    }
    while( ch>='0' && ch<='9' ) {
        x=x*10+ch-'0'; ch=getchar();
    }
    return flag ? -x : x;
}

inline int find(int x)
{
    if( fa[x]==-1 ) return 0;
    while( fa[x]!=x ) x=fa[x];
    return x;
}

inline void add(int x,int y)
{
    e[++cnte].to=y, e[cnte].nxt=head[x];
    head[x]=cnte; fa[y]=x;
}

inline int neo(int x,int i)
{
    val[i]=x; return i;
}

inline int merge(int x,int y)
{
    if( !x || !y ) return x+y;
    if( val[x]<val[y] || (val[x]==val[y] && x<y ) )  return add(x,y),x;
    return add(y,x),y;
}

inline void pop(int rt)
{
    int x,y;
    for(int i=head[rt]; i; i=e[i].nxt)
    if( fa[y=e[i].to]==rt ) q.push(y), fa[y]=y;
    fa[rt]=-1;
    while( q.size()>1 ) {
        x=q.front(), q.pop();
        y=q.front(), q.pop();
        q.push(merge(x,y));
    }
    if( !q.empty() ) q.pop();
}

inline void top(int x)
{
    if( !x ) { printf("-1
"); return; }
    printf("%d
",val[x]);
    pop(x);
}

int main()
{
    n=read(); m=read();
    for(int i=1; i<=n; ++i) fa[i]=neo(read(),i);
    int opt;
    for(int i=1; i<=m; ++i) {
        opt=read();
        if( opt==1 ) merge(find(read()),find(read()));
        else top(find(read()));
    }
    return 0;
}

  总结

  学完配对堆以后,我突然发现好像左偏树除了可持久化以外貌似一无是处了(复杂度完全被吊打233333)??????

  当然不是,因为左偏树结构非常稳定,方便套用其他不常用的一些操作,所以根据情况选择算法就行了,各种算法都各有各的优劣。(顺便推荐一波我的另一篇左偏树的博客23333

  最后附上一张各种堆比较的表格:

  

原文地址:https://www.cnblogs.com/cytus/p/9477876.html