Dijkstra 算法
参考:https://www.bilibili.com/video/BV1Ut41197ae?from=search&seid=7706418527991382536
参考:https://baike.baidu.com/item/%E8%BF%AA%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95/23665989?fr=aladdin
一,算法概述
1,概述
Dijkstra算法是由荷兰计算机科学家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。
算法解决的问题是:无负权的有向图的单源点最短路问题。
2,原理
保证边是非负的,那么长度长的最短路径一定是在长度短的最短路径的基础上延伸出来的。
因为:
假设 a 到 c 的最短路经过 b,显然 a 至 c 这条最短路必须由 a 至 b 的最短路和 b 至 c 的最短路组合起来才是最短的。(反证法很容易证明,这里就不给出了)
3,推论
由上述原理,可知,源点到所有点的最短路径集是一颗树。其中,根节点是源点,根节点到任意节点的路径为根节点到该节点的最短路径。
该树被称之为 —— 最短路径树。
4,注意点
* 最短路径树只必定存在于无负权的有向图的单源点最短路问题。
* 最短路径树并不唯一
二,算法思想
先利用上述推论,将图中的点分为两个集合:U (已落在最短路径树上的顶点集);V-U (未落在最短路径树上的顶点集)。
然后在连通 U 和 V-U 的所有边,求出边的权值加上该边属于 U 的点到源点的最路距离的总距离的最短距离,并将对应的连通边加入当前的最短路径树。
最后,不断循环选取,直至选取的边可以构成完整的最短路径树。
三,步骤及说明
设 N = {V,E} 是一连通网,M = {S,TE} 是 N 上的一棵最短路径树,n = | V |。
再设属于 V-U 的 点 i 只借助 U 中的点,到达源点的最短路径叫 —— 点 i 到源点的借助 S 最短路径。
① 初始化:令 S = { s },TE = { }。
② 更新距离:更新并记录 V-S 中的点到源点的借助 S 最短路径及其对应的距离。
③ 找最短边:在 V-S 中的点到源点的所有借助 S 最短路径中,找出距离最短的路径,进而找出该路径中连通 V-S 和 S 边 (ui,vi)。
④ 更新 M:将 (ui,vi) 加入 TE 中,并将 vi 从 V-S 中拿出,加入 S。
⑤ 迭代循环:重复 n-1 次 ②,③,④ 的循环。
说明1:为什么 ⑤迭代循环 要循环 n-1 次?
因为每次循环都要找到一个点到源点的最短路径,而除了自身之外还有 n-1 个点,所以需要循环 n-1 次。
说明2:为什么需要 ②更新距离?
因为每次 vi 加到 S 中,就可能会改变 V-S 中的点到源点的借助 S 最短路径,所以要遍历 V-S 中的点借助新增的 vi 到达源点的路径,比较是否会比之前的借助 S 最短路径更短。
说明3:为什么要先 ②,再 ③④?
条件:
每次进行 ④更新M 后,都必须进行 ②更新距离,具体见说明2。其实,①初始化 可以看成是一次 ④更新M,所以在 ①初始化 之后,需要进行一次 ②更新距离。
原因:
有些人的做法是在循环外进行 ②更新距离,然后按 ③④② 的顺序进行 n-1 次循环。
但其实最后一次循环的 ④更新M 之后进行的 ②更新距离 是没有意义的,因为此时所有点到达源点的最短路径已经都找出来了。
于是,我们利用这点,将②提到③④前面,从而将最后一次无用功的 ②更新距离 提前到第一次循环开头。利用第一次循环的 ②更新距离 对 ①初始化 进行更新距离,从而不用在循环外面特意更新距离。
四,数据结构
存图常用的链式前向星或邻接表或邻接矩阵,效果一样,效率不同。
dis[]:
算法进行时:
dis[i]:代表 点 i 到源点的借助 S 最短路径距离
算法结束后:
dis[i]:代表 点 i 到源点的最短路径距离
vis[]:
区分点是属于 S 还是 V-S:
若 vis[i] == 1,则代表 点 i 属于 S;
若 vis[i] == 0,则代表 点 i 属于 V-S
p[]:
表示 最短路径树:
若 p[i] == -2,则代表 i 是 s;
若 p[i] == -1,则代表 i 与 S 无通路;
若 p[i] != -2 && p[i] != -1,则 i 的父节点是 p[i];
五,代码及注意事项
1,源点的选取就是看你需要求到哪个点的最短路径。
2,初始化时 s 是源点。经过后面的更新和选择后,s 就代表循环中每次加入 S 中的新的点。
3,在 ②更新距离 中,p[] 是将 V-S 中的点到源点的所有借助 S 最短路径中的连通 V-S 和 S 边 (ui,vi) 全部记录下来。虽然,每次记录下来的时候,只有1条边是正确的。但在进行 ⑤迭代循环 后,p[] 记录的就都是最短路径树的边了。
4,如果在某次 ③找最短边 中,如果找到的边的权值是 inf,说明此时 S 和 V-S 之间不存在连通的边,即此时 V-S 的所有点与源点之间都不存在路径,也就不存在所谓的最短路径了。
5,如果是运行了多次的 Dijk() 算法的话,p[] 的初始化需要将所有的 p[i] 赋值为 -1。因为最短路径的图是不一定是连通的。而如果不是连通图,则 p[] 则会遗留下上一次 Dijk() 算法的值,没有被重新覆盖,导致找错路。
6,p[] 表示的是最短路径树,其根节点是源点。
如果代码中是 p[to] = s; —— V-S 中的点指向 S 中的点,则树的方向是叶子节点指向根节点。
如果代码中是 p[s] = to; —— S 中的点指向 V-S 中的点,则树的方向是根节点指向叶子节点。
1,链式前向星
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 100 #define inf 0x3f3f3f3f int dis[N], vis[N], p[N]; struct Chain_forward_star // 链式前向星 { int last[N], tot; struct Edge{ int pre, to, w; }e[N*N]; void init() { tot = 0; memset(last, -1, sizeof(last)); } void add(int from, int to, int w) { tot++; e[tot].to = to; e[tot].w = w; e[tot].pre = last[from]; last[from] = tot; } }star; void find(int i) // 对 p 进行回溯后输出 { if (p[i] == -1) { printf("源点与该点无通路."); return; } if (p[i] != -2) { find(p[i]); printf("->%d", i); } if (p[i] == -2) printf("%d", i); } void dijkstra(int n) { // ① 初始化 memset(vis, 0, sizeof(vis)); memset(dis, 0x3f, sizeof(dis)); memset(p, -1, sizeof(p)); int s = 1; dis[s] = 0; vis[s] = 1; p[s] = -2; int m = n - 1; while (m--) { // ② 更新距离 for (int i = star.last[s]; ~i; i = star.e[i].pre) { int to = star.e[i].to, w = star.e[i].w; if (!vis[to] && dis[s] + w < dis[to]) { dis[to] = dis[s] + w; // ④ 更新 M p[to] = s; } } // ③ 找最短边 int min = inf; for (int i = 1; i <= n; i++) { if (!vis[i] && dis[i] < min) { min = dis[i]; s = i; } } // ④ 更新 M vis[s] = 1; } // 输出 最小路径树 for (int i = 1; i <= n; i++) { if (p[i] == -2) continue; printf("源点 到 v%d 的最短距离为:%2d,最短路径为:", i, dis[i]); find(i); puts(""); } } int main(void) { int n, m; // 点数 和 边数 while (scanf("%d%d", &n, &m) != EOF) { star.init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); star.add(u, v, w); if (u != v) star.add(v, u, w); } dijkstra(n); } return 0; } /* 7 10 1 3 8 3 4 5 4 5 6 1 5 30 1 2 13 2 7 7 2 6 9 5 6 2 6 7 17 1 7 32 结果: 源点 到 v2 的最短距离为:13,最短路径为:1->2 源点 到 v3 的最短距离为: 8,最短路径为:1->3 源点 到 v4 的最短距离为:13,最短路径为:1->3->4 源点 到 v5 的最短距离为:19,最短路径为:1->3->4->5 源点 到 v6 的最短距离为:21,最短路径为:1->3->4->5->6 源点 到 v7 的最短距离为:20,最短路径为:1->2->7 */
2,邻接表 + 优先队列
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #include<queue> using namespace std; #define N 110 struct Node // 存点的信息,并将 < 号重载 { int id; // 点的编号 int dis; // 点到源点的最短距离 Node(int a, int b) :id(a), dis(b) {} friend bool operator < (const Node &a, const Node &b) { return a.dis > b.dis; } }; vector<Node>v[N]; int dis[N], vis[N], p[N]; void Dijkstra() { // ① 初始化 memset(vis, 0, sizeof(vis)); memset(dis, 0x3f, sizeof(dis)); memset(p, -1, sizeof(p)); priority_queue<Node>q; int src = 1; q.push(Node(src, 0)); dis[src] = 0; p[src] = -2; // ⑤ 迭代循环 while (q.size()) { Node vertex = q.top(); q.pop(); // ④ 更新 M vis[vertex.id] = 1; // ② 更新距离 for (int i = 0; i < v[vertex.id].size(); i++) { Node next = { v[vertex.id][i].id, v[vertex.id][i].dis + vertex.dis }; if (!vis[next.id] && next.dis < dis[next.id]) { dis[next.id] = next.dis; q.push(next); // ④ 更新 M p[next.id] = vertex.id; } } } } void find(int i) // 对路径回溯后输出 { if (p[i] == -1) { printf("源点与该点无通路."); return; } if (p[i] != -2) { find(p[i]); printf("->%d", i); } if (p[i] == -2) printf("%d", i); } int main(void) { int n, m; // 点数 和 边数 while (scanf("%d%d", &n, &m) != EOF) { // 清空邻接表 for (int i = 0; i <= n; i++) v[i].clear(); // 存图 for (int i = 0; i < m; i++) { int x, y, w; scanf("%d%d%d", &x, &y, &w); v[x].push_back(Node(y, w)); if (x != y) v[y].push_back(Node(x, w)); } Dijkstra(); for (int i = 1; i <= n; i++) { if (p[i] == -2) continue; printf("源点 到 v%d 的最短距离为:%2d,最短路径为:", i, dis[i]); find(i); puts(""); } } return 0; } /* 7 10 1 3 8 3 4 5 4 5 6 1 5 30 1 2 13 2 7 7 2 6 9 5 6 2 6 7 17 1 7 32 结果: 源点 到 v2 的最短距离为:13,最短路径为:1->2 源点 到 v3 的最短距离为: 8,最短路径为:1->3 源点 到 v4 的最短距离为:13,最短路径为:1->3->4 源点 到 v5 的最短距离为:19,最短路径为:1->3->4->5 源点 到 v6 的最短距离为:21,最短路径为:1->3->4->5->6 源点 到 v7 的最短距离为:20,最短路径为:1->2->7 */
3,邻接矩阵
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 110 #define inf 0x3f3f3f3f int a[N][N]; int dis[N], vis[N], p[N]; void find(int i) { if (p[i] == -1) { printf("此路不通"); return; } if (p[i] != -2) { find(p[i]); printf("->%d", i); } else printf("%d", i); } void dij(int n) { memset(dis, 0x3f, sizeof(dis)); memset(vis, 0, sizeof(vis)); memset(p, -1, sizeof(p)); int s = 1; p[s] = -2; dis[s] = 0; vis[s] = 1; int m = n - 1; while (m--) { for (int j = 1; j <= n; j++) { if (!vis[j] && dis[s] + a[s][j] < dis[j]) { dis[j] = dis[s] + a[s][j]; p[j] = s; } } int min = inf; for (int i = 1; i <= n; i++) { if (!vis[i] && dis[i] < min) { min = dis[i]; s = i; } } vis[s] = 1; } for (int i = 1; i <= n; i++) { printf("源点 到 v%d 的最短距离为:%2d,最短路径为:", i, dis[i]); find(i); puts(""); } } int main(void) { int n, m; while (scanf("%d%d", &n, &m) != EOF) { memset(a, 0x3f, sizeof(a)); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); a[u][v] = w; a[v][u] = w; } dij(n); } system("pause"); return 0; }
五:时间复杂度:
第一种: 用 邻接矩阵 存图,是通过循环点取边,所以这种算法适合 点少边多的稠密图,时间复杂度 O ( V*V)
第二种:用 链式前向星 存图,是通过循环边取点,所以这种算法适合 边少点多的稀疏图,时间复杂度 O ( V + ElogV )
六,Dijkstra 与 Prim 比较
相同点:
Dijkstra 和 Prim 都是从一个源点开始,不断在剩下的点中选取满足一定条件的点,加入自身,并以此将图的点集分成两个点集。
其中,对于 “一定条件” 也都是寻求两个集合的某种距离关系,并将距离最短的点加入源点归属的集合中。
差别点:
Dijkstra 的结果是 —— 最短路径树;
Prim 的结果是 —— 最小生成树。
Dijkstra 选择的条件是 —— V-S 中的点到源点的所有借助 S 最短路径中的最短路径中连通 V-S 和 S 的边;
Prim 选择的条件是 —— V-U 中的点到 U 的所有最短直达边中边长最短的边。
七,例题
使用 Dijkstra 算法求最短路,并在进行路径调整的时候同时调整最大救援队数量和最短路径数量。
用 num[] 记录每个点的救援队数量
用 cnt1[] 源点到点 i 的最短路径条数
用 cnt2[] 源点到点 i 的最大救援队数量
如果新出现的最短路径 == 原先最短路径
说明最短路径的选择变多了
所以最短路径数 等于 两条最短路径的叠加值;
所以最大救援队数量 等于 两条最短路径的较大的最大救援队数量。
如果新出现的最短路径 < 原先最短路径
说明最短路径只能还是新出现的最短路径
所以最短路径数 等于 新出现的最短路径数
所以最大救援队数量 等于 新出现的最短路径的最大救援队数量。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 505 #define inf 0x3f3f3f3f struct Chain_forward_star { int tot, last[N]; struct Edge { int pre, to, w; }e[N*N]; void init() { tot = 0; memset(last, -1, sizeof(last)); } void add(int from, int to, int w) { tot++; e[tot].to = to; e[tot].w = w; e[tot].pre = last[from]; last[from] = tot; } }star; int dis[N], vis[N]; int num[N]; // 每个点的救援队数量 int cnt1[N]; // 源点到点i 的最短路径条数 int cnt2[N]; // 源点到点i 的最大救援队数量 void dij(int s, int n) { memset(dis, 0x3f, sizeof(dis)); memset(vis, 0, sizeof(vis)); memset(cnt1, 0, sizeof(cnt1)); memset(cnt2, 0, sizeof(cnt2)); dis[s] = 0; vis[s] = 1; cnt1[s] = 1; cnt2[s] = num[s]; int m = n - 1; while (m--) { for (int i = star.last[s]; ~i; i = star.e[i].pre) { int to = star.e[i].to; int w = star.e[i].w; if (!vis[to]) { if (dis[s] + w < dis[to]) { dis[to] = dis[s] + w; cnt1[to] = cnt1[s]; cnt2[to] = cnt2[s] + num[to]; } else if (dis[s] + w == dis[to]) { cnt1[to] += cnt1[s]; if (cnt2[s] + num[to] > cnt2[to]) cnt2[to] = cnt2[s] + num[to]; } } } int min = inf; for (int i = 0; i < n; i++) { if (!vis[i] && dis[i] < min) { min = dis[i]; s = i; } } vis[s] = 1; } } int main(void) { int n, m, s, e; while (scanf("%d%d%d%d", &n, &m, &s, &e) != EOF) { star.init(); for (int i = 0; i < n; i++) scanf("%d", &num[i]); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); star.add(u, v, w); if (u != v) star.add(v, u, w); } dij(s, n); printf("%d %d\n", cnt1[e], cnt2[e]); } return 0; }
========= ======== ======== ====== ====== ===== ==== === == =
人没有牺牲就什麽都得不到,为了得到什麽东西,就需要付出同等的代价。
—— 钢炼