摆子复活了。

摆子写了篇笔记。

摆子又死了。


前言

为了做ADS的Project学的。难度堪比LCT,不过还是比红黑树略逊一筹。

斐波那契堆拥有及其优秀的理论复杂度:$O(1)$插入和减小键值,$O(\log n)$删除最值。然而也只是理论复杂度了。实际上堆里的斐波那契堆就好比平衡树里的splay,有个巨大的常数,所以只是个嘴巴算法。


抄袭来源

https://blog.51cto.com/u_13393656/5093051

https://blog.csdn.net/Foliciatarier/article/details/57472656

https://www.luogu.com.cn/blog/Atalod/learning-note-fibonacci-heap#


#define

  • $key$:节点排序的键值
  • 度数:节点的儿子数
  • $flag$:Decrease Key中使用的标记,用于记录该节点是否有儿子被剪切过。加入根链表中的节点自动清空$flag$。
  • $fib_n$:斐波那契数列第n项

斐波那契堆

以最小堆为例。

斐波那契堆跟二项堆很像,是由多个最小堆组成的森林。所有堆的根节点用循环双向链表链接,形成根链表。额外存储一个指针$\min$指向根链表中$key$最小的节点。

偷一张图:

Insert

往斐波那契堆中插入一个新节点。

直接把节点插入到根链表的结尾。因为根链表是循环双向链表,所以直接插入到$\min$的前一位即可。之后检查新节点的$key$是否小于$\min$的$key$,如果是则更新$\min$为新节点。

Delete Min

删除斐波那契堆中$key$最小的节点(即$\min$节点)。

首先新建一个临时存放子树的区域,叫做temp区域。把$\min$的所有子节点和根链表中$\min$以外的节点进行以下操作:

  1. 将节点带着其子树加入temp区域
  2. 检查temp区域中是否存在度数相同的树,如果有,合并两棵子树。具体的说,把$key$较小的根节点作为父,$key$较大的作为子,建立父子关系。
  3. 重复第2步直到不存在度数相同的树

随后,把temp区域中所有根节点用链表链接作为新的根链表,并更新$\min$节点,此时temp区域里即为删除完成的堆。

再偷一张图:

Decrease Key

将斐波那契堆中指定节点的$key$减小。

如果减小后的$key$仍然比父节点的$key$大,则啥都不用做。

否则该节点违反了堆的性质,执行以下“级联剪切”操作:

  1. 如果目标节点在根链表中,退出
  2. 断开目标节点与其父节点的父子关系
  3. 将目标节点带着子树加入根链表中,并更新$\min$节点
  4. 如果目标节点原来的父节点的未被标记$flag$,则标记;否则清除其标记并对其进行级联剪切

图不好,不偷了

其他

斐波那契堆还支持合并、Increase Key、删除任意节点等操作。因为Dijstra用不到就不写了


复杂度

假设有一斐波那契堆$H$,定义其势函数为$\Phi(H)=t(H)+2m(H)$,其中$t(H)$为根链表中节点的数目,$m(H)$为堆中被标记节点的数目。操作后$H$变为$H’$。

则进行一次操作的摊还代价为$T+\Phi(H’)-\Phi(H)$,其中$T$为该次操作实际代价。

Insert

一眼丁真,鉴定为$O(1)$。

Delete Min

假设在有$n$个节点的斐波那契堆中,任意节点的最大度数为$D(n)$。

在删除操作中,我们先是提取出删除节点的子节点加入temp区域(最多有$D(n)$个),再把根链表的节点全部加入temp区域(有$t(H)-1$个)。其中,由于合并两棵树是$O(1)$的,所以加入temp区域这一操作是$O(1)$的,则实际代价为$O(D(n)+t(H))$

删除后,势函数最坏情况为没有被标记节点加入根链表(否则$m(H’)$会减少)。因为相同度数的节点都被合并了,所以删除后根链表最多有$D(n)$个节点,则操作后势函数为$D(n)+2m(H)$。

一个sb问题:为什么删除后根链表最多有$D(n)$个节点?

A:因为删除后根链表里的节点度数两两不同,而$D(n)$定义就是根链表中最大的度数,所以最坏情况下就是度数为$1,2,\dots D(n)$的节点各有一个,最多$D(n)$个

摊还代价为$\\O(D(n)+t(H))+D(n)+2m(H)-t(H)-2m(H)\\=O(D(n))+D(n)+O(t(H))-t(H)\\=O(D(n))$

接下来问题变为$D(n)$是多少。

假设度数为$k$的节点最少有$f_k$个节点。一个度数为$k$的节点$p$可以拆分成一个度数为$k-1$的节点和其度数最大的子节点$p_{max}$。

由于只有在删除操作中两个节点度数相同时会建立父子关系,且操作后父节点度数只会+1,所以$p$的子节点度数为$1,2,\dots k-1$(不考虑Decrease Key)。考虑Decrease Key操作,因为一个子节点最多会删掉一个孙子节点(再删就被切走了),所以$p_{max}$的度数最小为$k-2$。

又一个sb问题:但是$p$也可能删掉子节点,如果把$p_{max}$删掉了,最大度数就不是$k-2$了。

A:删掉后$p$的度数也会-1啊…

综上,有$f_k\ge f_{k-1}+f_{k-2}$。

这个式子跟斐波那契数列很像,于是有$f_k\ge fib_k$。

即度数为$k$的节点子树最少有$fib_k$个节点。而斐波那契数列增长速度是指数级的,所以$D(n)=O(\log n)$,即删除操作均摊复杂度为$O(\log n)$。

Decrease Key

显然一次剪切操作是$O(1)$。

如果修改后违反了堆的性质:

还是用势函数分析。

假设一次操作触发了$c$次剪切,则实际代价为$O(c)$。根链表中多了被剪切的$c$个节点,则$t(H’)=t(H)+c$。除了剪切被修改节点,其余每次剪切清除一个标记,最后一个被剪切的节点的父节点可能被标记,则$m(H’)=m(H)-c+2$。

所以摊还代价为$\\O(c)+t(H’)+2m(H’)-t(H)-2m(H)\\=O(c)+t(H)+c+2m(H)-2c+4-t(H)-2m(H)\\=4\\=O(1)$


优化Dijstra

传统的堆优化Dijstra是靠插入新节点和打标记来实现Decrease Key的,所以复杂度在$m$上有个$\log$。

但是斐波那契堆可以在更新$dis$时直接对对应节点进行$O(1)$的$Decrease Key$,复杂度为$O(n\log n+m)$。并且由于该操作不增加新的节点,所以可以保证堆的空间复杂度从$O(m)$变为$O(n)$。虽然存边还是有个O(m)的空间


代码

基本纯手搓,很丑常数大还可能有bug。不过至少能跑过洛谷的最短路。

#include <stdio.h>
#include <stdlib.h>

#define MAXN 100005
#define INF 0x7f7f7f7f

/*----- Fibonacci堆 声明 BEGIN -----*/ 
struct FibonacciNode{
    struct FibonacciNode *left;//根链表的前驱或左兄弟
    struct FibonacciNode *right;//根链表的后继或右兄弟
    struct FibonacciNode *fa;//父节点
    struct FibonacciNode *child;//第一个孩子
    int key;//权值
    int ref;//该节点代表的图中的节点 
    int flag;//标记
    int degree;//度数
};
typedef FibonacciNode *FibNode;
FibNode min;
FibNode temp_min;//删除时使用的临时最小节点
FibNode temp[MAXN];//删除时使用的临时数组,temp[i]存储当前度数为i的节点
int max_degree;//删除时记录的最大度数
int max(int a,int b);
FibNode newNode(int key);//创建一个新节点,权值为key
void swap(FibNode *a,FibNode *b);
void link(FibNode pre,FibNode next);//连接两个节点,pre在前,next在后
void cut(FibNode p);//级联剪切节点p
void mark(FibNode p);//标记节点p,若已被标记则级联剪切p
void addNode(FibNode p);//将节点p插入到根链表中
void addTemp(FibNode p);//将节点p加入进temp数组中
void addChild(FibNode fa,FibNode ch);//将节点ch设为节点fa的儿子
int getMin();//获取堆中最小值对应的图中节点编号 
int isEmpty();//询问当前堆是否为空 
FibNode insert(int key,int ref);//插入一个节点,权值为key,代表图中节点ref 
void deleteMin();//删除最小节点
void decreaseKey(FibNode p,int key);//将节点p的权值改为key,且保证新的key比原来小
/*----- Fibonacci堆 声明 END -----*/ 

/*----- Dijstra 声明 BEGIN -----*/ 
struct EdgeNode{
    struct EdgeNode *next;
    int v,l;
};
typedef EdgeNode *Edge;
Edge first[MAXN];
FibNode node[MAXN];//图中节点在堆中对应的节点 
int n,m,s;//节点数,边数,起点 
int dis[MAXN];
void addEdge(int x,int y,int l);
void dijstra();
/*----- Dijstra 声明 END -----*/ 

int main(){
    scanf("%d%d%d",&n,&m,&s);
    while(m--){
        int x,y,l;
        scanf("%d%d%d",&x,&y,&l);
        addEdge(x,y,l);
    }
    dijstra();
    for(int i=1;i<=n;++i)
        printf("%d ",dis[i]);
}

/*----- Fibonacci堆 定义 BEGIN -----*/ 
int max(int a,int b){return a>b?a:b;}
FibNode newNode(int key){
    FibNode p=(FibNode)malloc(sizeof (struct FibonacciNode));
    p->left=p->right=p;
    p->fa=p->child=NULL;
    p->key=key;
    p->flag=0;
    p->degree=0;    
}
void swap(FibNode *a,FibNode *b){
    FibNode temp=*a;
    *a=*b;
    *b=temp;
}
void link(FibNode pre,FibNode next){
    pre->left=next->left;
    next->left->right=pre;
    next->left=pre;
    pre->right=next;
}
int getMin(){
    return min->ref;
}
int isEmpty(){
    return min==NULL;
}
void cut(FibNode p){
    if(p->fa==NULL)return;
    if(p->fa->child==p){
        p->fa->child=p->right;
        if(p->right!=NULL)p->right->left=NULL;
    }
    else {
        p->left->right=p->right;
        if(p->right!=NULL)p->right->left=p->left;
    }
    mark(p->fa);
    p->fa=NULL;
    addNode(p);
}
void mark(FibNode p){
    if(p->fa==NULL)return;
    if(p->flag)cut(p);
    else p->flag=1;
}
void addNode(FibNode p){
    p->flag=0;
    if(min==NULL)min=p->left=p->right=p;
    else{
        link(p,min);
        if(min->key>p->key)min=p;
    }
}
void addTemp(FibNode p){
    int d=p->degree;
    p->left=p->right=p;
    p->fa=NULL;
    if(temp[d]==NULL){
        max_degree=max(max_degree,d);
        temp[d]=p;
    }
    else {
        if(temp[d]->key>p->key)swap(&temp[d],&p);
        addChild(temp[d],p);
        addTemp(temp[d]);
        temp[d]=NULL;
    }
}
void addChild(FibNode fa,FibNode ch){
    if(fa->child!=NULL){
        ch->right=fa->child;
        fa->child->left=ch;
    }
    else ch->right=NULL;
    fa->child=ch;
    ch->left=NULL;
    ch->fa=fa;
    max_degree=max(max_degree,++fa->degree);
}
FibNode insert(int key,int ref){
    FibNode p=newNode(key);
    p->ref=ref;
    addNode(p);
    return p;
}
void deleteMin(){
    max_degree=0;
    temp_min=NULL;
    if(min->child!=NULL)
        for(FibNode p=min->child,next=p;p!=NULL;p=next)next=next->right,addTemp(p);
    for(FibNode p=min->right,next=p->right;p!=min;p=next,next=next->right)addTemp(p);
    free(min);
    min=NULL;
    for(int i=0;i<=max_degree;++i)
        if(temp[i]!=NULL)addNode(temp[i]),temp[i]=NULL;
}
void decreaseKey(FibNode p,int key){
    p->key=key;
    if(p->fa==NULL){
        if(min->key>key)min=p;
    }
    else if(key<p->fa->key)cut(p);
}
/*----- Fibonacci堆 定义 END -----*/ 

/*----- Dijstra 定义 BEGIN -----*/
void addEdge(int x,int y,int l){
    Edge e=(Edge)malloc(sizeof (struct EdgeNode));
    e->v=y;
    e->l=l;
    e->next=first[x];
    first[x]=e;
}

void dijstra(){
    for(int i=1;i<=n;++i){
        if(i!=s)dis[i]=INF;
        node[i]=insert(dis[i],i);        
    }
    while(!isEmpty()){
        int x=getMin();
        deleteMin();
        for(Edge e=first[x];e!=NULL;e=e->next){
            int y=e->v;
            if(dis[x]+e->l<dis[y]){
                dis[y]=dis[x]+e->l;
                decreaseKey(node[y],dis[y]);
            }
        }
    }
}
/*----- Dijstra 定义 END -----*/

不写数组改写指针了,函数声明和定义分开了,终究是被大学磨平了棱角

话说c是真tm难用,没了面向对象我怎么活啊