单源最短路径 の 二叉堆优化的 Dijkstra

🚧🚧 SPFA 它死了 🚧🚧

可以通过构造数据使 SPFA 算法的时间复杂度退化到 \(O(NM)\) 和 Bellman-Ford 算法已经没什么区别了...QwQ

二叉堆优化的 Dijkstra

  • 对于 无负边 的情况下可以达到 \(O(nlog(n))\) 且很难被卡
  • Dijkstra是基于一种 贪心 的策略,首先用数组 dis 记录起点到每个结点的最短路径,再用一个数组保存已经找到最短路径的点
    然后,从 dis 数组选择最小值,则该值就是源点 s 到该值对应的顶点的最短路径,并且把该点记为已经找到最短路
    此时完成一个顶点,再看这个点能否到达其它点(记为 v ),将 dis[v] 的值进行更新
    不断重复上述动作,将所有的点都更新到最短路径
  • 这种算法实际上是 \(O(n^2)\) 的时间复杂度,但我们发现在 dis 数组中 选择最小值 时,我们可以用 小根堆 来进行优化。
  • Dijkstra 每次进行 选择距离起点最近的顶点 需要 \(O(N)\) 的时间,这样每次都需要耗费很长时间选择, 使用 小根堆 进行优化,使每次选择时间降至 \(O(logN)\)

蒟蒻代码

// 二叉堆优化的 Dijkstra
#include <bits/stdc++.h>
#define re register
using namespace std;
typedef long long ll;

// 链式向前星
struct Edge{
    int to;     // x->当前节点的节点编号
    int nxt;    // x->当前节点对应的数组索引
    int val;    // 边权
};

// 优先队列元素
struct node{
    int ver;    // 顶点编号
    ll dis;     // 到 s 的距离, 用于小根堆排序
    // 重载 < 运算符
    bool operator <(const node& x) const{
        return dis>x.dis;   // 小根堆 所以是 >
    }
};

const int N=1e5+5;
const int M=2e5+5;

int n,m,s;

// 存图
int head[N];
Edge edge[M];
int tot=0;

// 加边(起点, 终点, 边权)
void add(int x,int y,int w){
    edge[++tot].to=y;
    edge[tot].val=w;
    edge[tot].nxt=head[x];
    head[x]=tot;
}

bitset<N> vis;
// 优先队列内部实质是一个 小根堆
priority_queue<node> pq;
ll dis[N];

int main()
{
    ios::sync_with_stdio(0);
    clock_t c1 = clock();
#ifdef LOCAL
    freopen("data.in","r",stdin);
    freopen("data.out","w",stdout);
#endif
    // ======================================================================
    memset(dis,0x3f,sizeof(dis));   // 初始化极大值
    cin>>n>>m>>s;
    for(re int i=0;i<m;i++){
        int a,b,c; cin>>a>>b>>c;
        add(a,b,c);
    }

    // 算法主体
    dis[s]=0; pq.push((node){s,0}); // s 入堆
    while(!pq.empty()){ // 堆中还有可更新的元素
        int x=pq.top().ver;
        // int d=pq.top().dis; 不必取出, 仅用来给 priority_queue 排序
        pq.pop();
        // 标记蓝白点
        if(vis[x]) continue;
        vis[x]=1;
        // 与 x 相邻的可更新的点全部入堆...怎么感觉有点SPFA的意思在里面
        for(re int i=head[x]; i; i=edge[i].nxt){
            int y=edge[i].to;
            int w=edge[i].val;
            if(dis[x]+w<dis[y]){
                dis[y]=dis[x]+w;
                pq.push((node){y,dis[y]});  // 入堆
            }
        }
    }

    for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
    // ======================================================================
end:
    cerr << "Time Used:" << clock() - c1 << "ms" << endl;
    return 0;
}

这里采用的是 懒惰删除 的思想。因为STL的优先队列不支持对内部元素的随机删除,所以采用一个标记。如果曾经使用过,那么在它到堆顶的时候就不使用它。相当于把删除操作延迟到堆顶进行。

一些东西

priority_queue 声明方法

priority_queue 内部是 二叉堆 , 本身有三种声明方法:

priority_queue<int> pq;	//默认大根堆
priority_queue<int, vector<int>, less<int> > pq;	// 大根堆
priority_queue<int, vector<int>, greater<int> > pq;	// 小根堆

但一般 priority_queue 用自己定义的结构体类型时, 都要重载 < 运算符, 便于内部二叉堆排序比较

大根堆

// 优先队列元素
struct node{
    int ver;    // 顶点编号
    ll dis;     // 到 s 的距离, 用于小根堆排序
    // 重载 < 运算符
    bool operator <(const node& x) const{
        return dis<x.dis;   // 大根堆 所以是 <
    }
};

priority_queue<node> pq;	// 大根堆

小根堆

// 优先队列元素
struct node{
    int ver;    // 顶点编号
    ll dis;     // 到 s 的距离, 用于小根堆排序
    // 重载 < 运算符
    bool operator <(const node& x) const{
        return dis>x.dis;   // 小根堆 所以是 >
    }
};

priority_queue<node> pq;	// 小根堆
posted @ 2021-08-22 19:09  不爱喝橙子汁的橙子  阅读(132)  评论(0编辑  收藏  举报