Dijkstra算法简述

145 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天

Dijkstra算法

解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。

注意:Dijkstra 算法要求图中不能出现负权边。

①、Dijkstra算法流程

我们定义带权图 G 所有顶点的集合为 V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U,初始集合 U 为空,记从源点 s 出发到每个顶点 v 的距离为 dist_v ,初始 dist_s=0。接着执行以下操作:

  1. 从 V−U 中找出一个距离源点最近的顶点v,将v加入集合 U。
  2. 并用 dist_v和顶点 v 连出的边来更新和 v 相邻的、不在集合 U中的顶点的 dist,这一步称为松弛操作。
  3. 重复步骤 1 和 2,直到 V=U或找不出一个从 s 出发有路径到达的顶点,算法结束。

如果最后 V \neq U,说明有顶点无法从源点到达;否则每个 dist_i表示从 s 出发到顶点 i 的最短距离。

Dijkstra 算法的时间复杂度为 \mathcal{O}(V^2),其中 V 表示顶点的数量。

Dijkstra 是解决无负边权的图的单源最短路问题,经常使用邻接表存储。

不优化的时间复杂度是 O(V^2 + E)

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, fail;
    edge() {}
    edge(int _v, int _w, int _fail) {
        v = _v;
        w = _w;
        fail = _fail;
    }
} e[M << 1];
int head[N], len;
void init() {
    memset(head, -1, sizeof(head));
    len = 0;
}
void add(int u, int v, int w) {
    e[len] = edge(v, w, head[u]);
    head[u] = len++;
}
void add2(int u, int v, int w) {
    add(u, v, w);
    add(v, u, w);
}
int n, m;
int dis[N];
bool vis[N];
void dijkstra(int u) {
    memset(vis, false, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    dis[u] = 0;
    for (int i = 0; i < n; ++i) {
        int mi = inf;
        for (int j = 1; j <= n; ++j) {
            if (!vis[j] && dis[j] < mi) {
                mi = dis[u = j];
            }
        }
        if (mi == inf) {
            return;
        }
        vis[u] = true;
        for (int j = head[u]; ~j; j = e[j].fail) {
            int v = e[j].v;
            int w = e[j].w;
            if (!vis[v] && dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
            }
        }
    }
}
int main() {
    init();
    int u, v, w;
    cin >> n >> m;
    while (m--) {
        cin >> u >> v >> w;
        add2(u, v, w);
    }
    dijkstra(1);
    cout << dis[n] << endl;
    return 0;
}

②、基于小根堆优化的Dijkstra算法

用一个set来维护点的集合,这样的时间复杂度就优化到了 \mathcal{O}((V+E)\log V),对于稀疏图的优化效果非常好

const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}
typedef pair<int, int> PII;
set<PII, less<PII> > min_heap;
int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    min_heap.insert(make_pair(0, s));
    dist[s] = 0;
    for (int i = 0; i < n; ++i) {
        if (min_heap.size() == 0) {  // 如果小根堆中没有可用顶点,说明有顶点无法从源点到达,算法结束
            return false;
        }
        // 获取堆顶元素,并将堆顶元素从堆中删除
        set<PII, less<PII> >::iterator iter = min_heap.begin();
        int v = iter->second;
        min_heap.erase(*iter);
        vst[v] = true;
        // 进行和普通 dijkstra 算法类似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {
                // 先将对应的 pair 从堆中删除,再将更新后的 pair 插入堆
                min_heap.erase(make_pair(dist[x], x));
                dist[x] = dist[v] + e[j].w;
                min_heap.insert(make_pair(dist[x], x));
            }
        }
    }
    return true;  // 存储单源最短路的结果
}

③、基于优先队列优化的Dijkstra算法

我们在 node 节点里面记录对应的点的最短路,然后每次更新一个点的最短路后都把这个点压入到优先队列里面(不管之前有没有被压入到队列里面),这样就一定能够保证优先队列对的性质不会改变

这个代码的时间复杂度实际上会比用真正的堆要慢一点,因为有的点可能会入队多次,但是每一条边最多导致一次入队,所以这个算法的时间复杂度为 \mathcal{O}(E\log E)。其中 E 为边的数量。

const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}
int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
struct node {
    int u;
  int dist;
    node(int _u, int _dist) : u(_u), dist(_dist) {}
    bool operator < (const node &x) const {
        return dist > x.dist;
    }
}; // 记录点的结构体
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    priority_queue<node> min_heap;
    dist[s] = 0;
    min_heap.push(node(s, 0));
    while (!min_heap.empty())
        // 获取堆顶元素,并将堆顶元素从堆中删除
        int v = min_heap.top().u;
        min_heap.pop();
        if (vst[v]) {
            continue;
        }
        vst[v] = true;
        // 进行和普通 dijkstra 算法类似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {
                dist[x] = dist[v] + e[j].w;
                min_heap.push(node(x, dist[x]));
            }
        }
    }
    return true;
}