专栏文章
Rank-Pairing Heap(赋级配对堆)学习笔记
算法·理论参与者 31已保存评论 37
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 37 条
- 当前快照
- 1 份
- 快照标识符
- @mhz5saxd
- 此快照首次捕获于
- 2025/11/15 01:55 4 个月前
- 此快照最后确认于
- 2025/11/29 05:24 3 个月前
Rank-Pairing Heap(rp-heap,赋级配对堆)是一种堆。(废话
复杂度和Fib堆相同(如果不知道Fib堆什么复杂度,百度,请)。
论文里说rp堆更优秀(理由是维护的信息更少),但wiki说Fib堆更快一些。
不过至少我觉得rp-heap要好写得多(论文里好像也这么说了)。毕竟我这种菜鸡都能写出来
还有EtaoinWu的《配对堆与赋级配对堆(Rank Pairing Heaps)》。本文的大部分中文译名参考自此。
先膜为敬,EtaoinWu Orz
本文中采用中文还是英文名似乎很混乱。
写blog不易,给个赞吧(
以下默认是最小堆(小根堆)。
前置知识 : 二叉树,渐进记号,摊还分析,初中一年级数学(废话
不必要的前置知识 : 配对堆,二项堆,Fib堆
说几句闲话,学习Fib堆可以参考洛咕谷日爆报 :
#214[木木!]安塔的二项堆&斐波那契堆学习笔记
但是个人认为这篇文章讲得不够详细(就连势函数都是不完整的),如果想详细了解,还是推荐算法导论,或者这篇blog。
0.一些记号
表示一个rp-heap,表示结点或数值,表示关键字。
和分别表示左儿子和右儿子。
文中提到"其它操作复杂度"时,不包括以上两操作的复杂度。
1.半树(half tree)
1.1.定义
rp-heap是由若干半树()组成的。
的定义是 : 每个结点的比左子树任意结点的小。
半树的定义是 : 根节点没有右儿子,且所有点满足性质的二叉树。(请仔细读这句话
由此可以立刻得出根是树中最小的。
1.2.
是半树的基本操作,用于把两棵半树合并成一棵半树。它的过程如下 :
接受两棵半树的根作为输入。不妨假设的更小。把的左儿子拿下来,接到的右儿子上,然后把接到的左儿子上。这是的。
这是从论文里拿的图 :

显然合并后仍然满足半树定义。
1.3.(级)
定义。单个节点的是。定义一棵半树的是根的值。
我们规定,值相同的半树能进行。操作之后,具有较大的根,其它结点不变。还是从论文拿的图 :

发现如果从若干只有一个结点的半树开始,不管怎样,产生的半树的形态都是根的左子树挂着一个满二叉树。
更进一步,发现这样的半树跟二项树本质是相同的。画个图解释一下 :

(希望dalao们可以给我一个能做这样动图的软件qwq)
所以这个数据结构可以看作二项堆的优化。(Fib堆似乎也是qwq)
实际上论文似乎就是这么认为的。
但由于复杂度分析里面用到了半树上的兄弟,使用二项树的理论可能会很难受。
2.rp-heap的结构
rp-heap使用一个称为根链表的双向循环链表串起一串的半树根(但是实际上根链表存在仅仅是为了方便插入删除,并不表示任何顺序),并且维护堆中有着最小值的结点的指针。显然,一定是某个根。
3.一个不够优秀的复杂度
对于和,我们直接连接/插入根链表。
对于,和二项堆一样,把删掉后,把它左子树的右链(或右脊柱,即从根一直走右儿子直到没有右儿子得到的路径)上所有边断开,得到若干棵半树(因为每个点都满足,并且断开右链后右链上的点失去了右儿子)。将这个操作称为(拆分)。
接下来把这些半树和根链表里的其它根一起进行一轮配对() : 对每个建立一个桶,先遍历产生的半树根再遍历根链表,把当前半树扔进对应的桶里。如果桶里已经有树,把它们起来,并删掉根链表里面两棵树的根,直接在根链表加入后的新根。最后把桶里剩下的也扔进根链表。
对于,为了达到,我们用被减值的结点的右子树替换它原来的位置,把它和它的左子树拿出来扔到根链表里,这个操作称为对的一次(切断)。这样我们才能不必让上浮。
但是它破坏了为的二项树至少有个结点的性质(你把一部分拆出来了,结点数就不够了),因此可以造出达到的半树,这样就需要使用个桶。
这个东西可以摊到除了是的(没有的情况下),其它都是的,即不带的Pairing Heap复杂度。因为我看不懂英文这不是重点,所以这里不给出证明。有兴趣可以自行阅读论文。
为了得到高效的,我们需要放宽一些限制,使是的性质不会被打破后难以复原,并在优秀的实现中保持该性质。
4.一个优秀的复杂度
定义,称左儿子为的点为一个-结点,两个儿子分别为(不分顺序)的点为一个-结点。
在之前的做法和标准的二项堆里,每个根是-结点,每个内部结点是结点。rp-heap的核心思想是放宽内部结点的限制。
我们引入两种,它们是rp-heap对半树的限制。
Type-1(甲类)条件 : 每个根是-结点,而每个内部结点要么是-结点,要么是-结点,其中是任意正整数。可以发现一棵满足Type-1条件的为的半树至少有个结点。
Type-2(乙类)条件 : 每个根是-结点,而每个内部结点是-结点,-结点或-结点,其中是任意正整数。可以发现一棵满足Type-2条件的为的半树至少有个结点。
因此不管使用哪一种,最大是的。
这不会影响前面其它操作的正确性。
接下来我们可以尝试实现了。
如果被减值结点是根,不用做任何结构操作。
否则,先对进行一次,并把设为。然后从原来的父亲开始,判断当前结点是否仍然满足,如果不满足,则把当前结点的减少到满足,并继续处理当前结点的父亲。如果没有破坏条件就结束。
这是论文中的图 : (图中是Type-1 rp-heap)

现在你知道这个数据结构名字的由来了,它使用类似于配对堆的配对方式,并用特殊的来放松二项堆对结点的限制,从而得到优秀的复杂度。
Fib堆也用到了类似的思想 : 在Fib堆里,如果一个结点要破坏使复杂度正确的性质时,就把它也拆下来,并把拆下来这部分的复杂度摊掉。一会你将看到,rp堆也会摊掉保持的代价。
5.复杂度分析
下面的内容比较毒瘤并且有很多显然,请自行选择是否看完。不想看可以向下翻到代码部分。
这里只分析常数更小的Type-2 rp-heap。对Type-1有兴趣的话,反正我不会请自行阅读论文。
定义一个结点的基本势()为,设是一个-结点(只有一个儿子则视),则基本势为,即。
(这个译名是我口胡的,如果不标准,希望神仙们可以指出来)
定义一个结点的额外势()为 :
-
如果是根,额外势为。
-
如果是-结点,额外势是。
-
否则,额外势是。
(这个译名也是我口胡的,如果不标准,也希望神仙们可以指出来)
定义一个结点的势为基本势和额外势的和。则立刻可得 :
-
如果是根,势为。
-
如果是-结点,势为。
-
否则,设是-结点,势为。
定义势函数为所有结点势的和。
和实际代价是的并且不改变势。和实际代价是的并且将势增加。所以这四个操作摊还复杂度都是。
考虑。定义为后半树的总数。显然,的实际代价是(是操作桶的时间,如果你每次最后遍历桶而不是记录非空桶的话)。考虑被删除根的左子树右链上最多有个-结点(每个使减少),它们变成根之后会增加最多的势。除此之外其它任何结点变成根都不会使势增加,所以使势增加最多。接下来进行一轮配对,在这棵半树中最少进行次,每一次会把一个根变成-结点,使势减少,因此配对至少使势减少。整个操作使势增加最多。我们可以把势的单位增加到可以消去实际代价,因此摊还复杂度是。
接下来是恶心的。假设我们对做了一次减值。
如果是根,实际代价是并且不改变势。
否则,我们就懒得往下分析了 定义且为在中维持时遇到的第一个不需要调整就满足的结点或是根(为根时是调整了根或根的儿子),定义为前除之外的儿子,即的兄弟。加表示后的(真省事)(就是说,举个例子,是前的,是后的)。
显然,这次操作的实际代价是。
显然,后势可能发生变化的只有。
显然,后的结构变化只有的原来是的那个儿子变成了,变成了根并失去了右儿子。
先考虑基本势变化,即的基本势变化。把它分成三部分。我们会在每部分最后列出一个式子,从它们可以推出关于基本势变化的结论。
-
考虑的和,在前它们在同一条链上,中间的部分拆开相消,得到。(说句闲话,这部分论文似乎出了点问题,把写成了)考虑的和,在后它们在同一条链上,中间的部分拆开相消,得到。因为且,。这部分的基本势减少了至少。(这是对的贡献,前对的贡献和后对的贡献)
-
显然,。这部分的基本势减少了至少。(这是对的贡献)
-
因为被调整了而没有,。因为不一定被调整了,。这部分的基本势减少了至少。(这是对的贡献)
我们已经计算了的每个儿子对它们的贡献。把它们加起来就得到基本势减少了至少,即增加最多。
然后考虑额外势变化。考虑额外势只会在-结点变成其它结点时,或是某个结点变成根时增加。
我们先证明在被调整的点中最多只有两个-结点。考虑调整过程中遇到的第一个-结点,它会被调整为-结点或-结点。对于第一种,它的没有改变,所以父亲的条件不会被破坏,所以它是最后一个被调整的结点,一共有一个-结点。对于第二种,它的减少了,因此接下来所有需要调整点的都只会减少(不理解可以自己枚举情况)。接下来遇到的第二个-结点只会被调整为-结点,所以它是最后一个被调整的结点,一共有两个-结点。因此,-结点最多有两个,最多使额外势增加。
在过程中,只有变成了根,使额外势增加。
把基本势和额外势的变化加起来,我们得到势的变化 : 势最多增加。我们可以把势的单位增大到可以消去实际代价,所以的摊还复杂度是。
6.代码实现
请参考放在最前面的EtaoinWu的blog。
或者往下翻,是我自己写的代码,有注释。用指针没有可读性
优化Dijkstra到是支持时间和,的堆的经典应用。
CPP//Type-2 Rank-Pairing Heap优化Dijkstra
//luogu P4779(请自行调整MAXN和MAXM)
//没有merge操作,写起来大概也不难(
#define MAXN 100000
#define MAXM 1000000
#define Log_Phi_N 29
//点数,边数,桶数量
#ifndef NULL
#define NULL 0
#endif
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
template<class T>
struct RP_Heap
{
struct ListNode;
struct Node
{
T val;
int rank;
Node *l,*r,*fa;
ListNode* p;
Node(){}
Node(T _val,int _rank=0,Node* _l=0,Node* _r=0,Node* _fa=0):val(_val),rank(_rank),l(_l),r(_r),fa(_fa){}
}t[MAXN+2],*p[MAXN+2],*min,*bucket[Log_Phi_N+1];
int cnt,siz;
bool has_min;
struct ListNode
{
Node* ptr;
ListNode *pre,*nxt;
};
struct List//手写链表
{
ListNode* head;
List()
{
head=new ListNode();
head->ptr=NULL;
head->nxt=head;
head->pre=head;
}
inline void insert(Node* _ptr)
{
ListNode* u=new ListNode();
u->ptr=_ptr;
u->pre=head;
u->nxt=head->nxt;
head->nxt->pre=u;
head->nxt=u;
_ptr->p=u;
}
inline void remove(ListNode* u)
{
u->pre->nxt=u->nxt;
u->nxt->pre=u->pre;
u->ptr->p=NULL;
delete u;
}
}list;
RP_Heap()
{
for(int i=0;i<MAXN;i++)
p[i]=&t[i];//p[i]指向t[i],便于分配空间
has_min=0;//has_min让extract-min之后不用把min赋为-inf
memset(bucket,0,sizeof(bucket));
}
inline Node* new_Node(T v)//这句话就是分配一个新结点,val为v的意思(
{
return ( & ( * p[cnt++] = Node(v,0,NULL,NULL,NULL) ) );
}
inline void swap(int &x,int &y){ int temp=x;x=y;y=temp; }
inline int max(int x,int y){ return x>y?x:y; }
inline int abs(int x){ return x>0?x:-x; }
inline void swap(Node* &x,Node* &y){ Node* temp=x;x=y;y=temp; }
inline Node* link(Node* u,Node* v)
{
if(v->val<u->val) swap(u,v);
v->r=u->l;
if(v->r)//记得判断是不是空指针,否则RE,以下类似的省略注释
v->r->fa=v;
u->l=v;
v->fa=u;
u->rank++;
return u;
}
inline Node* update_min(Node* u)//更新min,为了写起来舒服,返回值是给的指针
{
if(!has_min||u->val<min->val)
min=u,has_min=1;
return u;
}
inline Node* insert(T _val)
{
Node* u=new_Node(_val);
list.insert(update_min(u));//看出来update_min这么写舒服了qwq
siz++;
return u;
}
inline T find_min(){ return min->val; }
inline int size(){ return siz; }
inline bool empty(){ return siz==0; }
inline void extract_min()
{
siz--;
list.remove(min->p);
has_min=0;
int rk;
Node *next_node,*u;
ListNode *first=list.head->nxt;//第一个原有根位置
for(u=min->l;u;u=next_node)//拆分左子树右链
{
next_node=u->r;
u->fa=u->r=NULL;
rk=u->rank;
if(bucket[rk])//和桶里的合并,扔进根链表
list.insert(update_min(link(u,bucket[rk]))),bucket[rk]=NULL;
else//扔进桶
bucket[rk]=u;
}
for(ListNode *i=first,*next_node;i!=list.head;i=next_node)
{
u=i->ptr;
rk=u->rank;
next_node=i->nxt;
list.remove(i);
if(bucket[rk])
list.insert(update_min(link(u,bucket[rk]))),bucket[rk]=NULL;
else
bucket[rk]=u;
}
for(int i=0;i<=Log_Phi_N;i++)//遍历桶,把桶里的扔进根链表
if(bucket[i])
list.insert(update_min(bucket[i])),bucket[i]=NULL;
}
inline void decrease_key(Node* u,T key)//把u的值变成k(所以需要自己保证是减值不是增值)
{
u->val=key;
if(u->fa==NULL)//是根,更新min直接返回
{
update_min(u);
return;
}
if(u->l) u->rank=u->l->rank+1;//更新rank为左儿子rank+1
else u->rank=0;//没有左儿子,说明是单点
if(u->r) u->r->fa=u->fa;//把右儿子接到自己原来的位置
if(u==u->fa->l) u->fa->l=u->r;
else u->fa->r=u->r;
int temp,lrk,rrk;
for(Node* v=u->fa;v;v=v->fa)
{
lrk=v->l?v->l->rank:-1;//实现的小技巧,没有结点的rank视为-1
rrk=v->r?v->r->rank:-1;
temp=max(lrk,rrk)+(abs(lrk-rrk)<=1?1:0);//根据rank rule计算新的rank
if(temp==v->rank)//如果没有改变rank,break掉
break;
v->rank=temp;
}
u->fa=u->r=NULL;//最后断开父亲和右儿子是因为前面循环开头要用右儿子,懒得再搞临时变量了
list.insert(update_min(u));//插入根链表并更新min
}
};
//下面的是Dijkstra模板
struct Edge
{
int v,w,next;
}e[2*MAXM+2];
struct Node
{
int u;
int dis;
inline bool operator<(const Node &x) const
{
return this->dis<x.dis;
}
};
int h[MAXN+2],cnt=0;
inline void add_edge(int u,int v,int w)
{
e[++cnt]={v,w,h[u]};
h[u]=cnt;
}
int dis[MAXN+2];
int n,m,start,tu,tv,tw,to;
RP_Heap<Node> q;
RP_Heap<Node>::Node* node[MAXN+2];//对每个堆结点保留一个指针,才能进行decrease-key
inline void dij()//Dijkstra板子
//没有vis(表示是否已经扩展过的数组)是因为我们有decrease-key
{
int now;
for(int i=1;i<=n;i++)
dis[i]=i==start?0:0x7fffffff,node[i]=q.insert({i,dis[i]});
while(!q.empty())
{
now=q.find_min().u;
q.extract_min();
for(int i=h[now];i!=0;i=e[i].next)
if(dis[e[i].v]>dis[now]+e[i].w)
dis[e[i].v]=dis[now]+e[i].w,
q.decrease_key(node[e[i].v],{e[i].v,dis[e[i].v]});//减值
}
}
int main()
{
scanf("%d%d%d",&n,&m,&start);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&tu,&tv,&tw),add_edge(tu,tv,tw);
dij();
for(int i=1;i<=n;i++)
printf("%d ",dis[i]);
return 0;
}
7.说在后面&更新记录
看到这篇blog的dalao们,如果发现了什么问题,希望可以告诉我qwq
2020/4/15 完结撒花qwq
upd 2020/4/16: 增加了代码,修改了一些细节。
upd 2020/4/17: 修改了一些细节。连更三天可见有多少bug
upd 2020/4/22: 修改了一处错误。
upd 2020/5/22: 修改了一些细节。
upd 2020/9/20: 修改了一处错误。
相关推荐
评论
共 37 条评论,欢迎与作者交流。
正在加载评论...