Dijkstra算法求解单源最短路径问题
文章目录
- 一 前言
- 二 Dijkstra 算法讲解
- 1. 贪心算法的证明
- 2. 算法实现说明
- 3. 初版Dijkstra算法代码
- 三 时间复杂度优化
- 1. 优化策略
- 2. 优化后的代码
- 四 结语
一 前言
上一次的文章更新还是在5月份,现在都快8月份了,整整三个月没有记录学习的点点滴滴。哎,主要是现在自己面临读研的一系列事情,到现在面试结果也没有出来,如果没中,那真的要给老师,给招生办跪下了,呜~~~~~~, 小菜鸡在大佬面前瑟瑟发抖,给我一个读书的机会吧!!!
本次记录的是最短路径算法中的单源最短路经Dijkstra算法,也就是确定图中某一个点,求出图中其他所有点到该点的最短距离分别是多少。
二 Dijkstra 算法讲解
1. 贪心算法的证明
Dijkstra算法的基本思想是贪心思想,关于贪心算法求解问题时,我们需要进行证明两点:
- 最优子结构
- 局部最优性(局部最优解可以得到全局最优解)
关于算法的证明,小编就不多说了,这里直接分享一篇博客:迪杰斯特拉(Dijkstra)算法最通俗易懂的讲解 ,大家要是觉得解释的不是很好,最好还是去算法书上看看 。
2. 算法实现说明
在实现过程中,重要的就是我们引入了两个数组:dis数组和vis数组
dis数组:用来记录每个点到原点的最短路经。
vis数组:用来标记,某个点是否已经扩展过。
我们要做的就是在所有未扩展的点中,选出一个dis值最小的点进行松弛操作,这样n - 1次操作之后,即可知道原点到所有其他点的最短距离。
算法核心操作:
现在假设我们已知结点A到源点S的最短距离为X,也存在从结点A指向结点B的有向边,该边的权值为val,显然结点B到原点会有一个距离,我们假设为Y,若X + val < Y,换句话说就是,我们现在找到了源点S到结点B的更短的一个路径,此时我们更新dis数组中到结点B到源点的距离值。
现在我们模拟一遍算法的执行流程
- 首先初始化dis数组和vis数组,dis数组中源点的值为0,剩余点的dis值为极大值;vis数组中均为0 ,其中0表示没有扩展,1表示该点已被扩展。
- 遍历dis数组,找到值最小,且没有扩展(vis值为0)的点,此时为1号结点。观察图可知,1号结点可以到达2, 3号结点,所以计算可知,结点2到源点的距离为6 + 0 = 6 < dis[2],结点3到源点的距离为0 + 1 = 1 < dis[3], 所以2, 3号结点均可以被更新。至此1号结点已经被扩展,标记vis[1] = 1;
- 遍历dis数组,找到值最小,且没有扩展(vis值为0)的点,此时为3号结点。观察图可知,3号结点可以到达2,4,6号结点,计算可知:2号结点到源点的距离为3 + dis[3] = 3 + 1 = 4(连接3号结点和2号结点的边的权值为3,3号结点到源点的最小距离为1)这个新的距离比原来2号结点到源点的距离6要小,所以我们需要进行距离的更新。同理,我们更新4和6号结点的距离。
- 按照之前的规则,我们相似操作,最终,我们可以得到如下的结果
3. 初版Dijkstra算法代码
我们以洛谷第3371题为例子:P3371 【模板】单源最短路径(弱化版)
解题代码如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stack>
#include <algorithm>
#include <string>
#include <map>
#include <set>
#include <vector>
#include <cstring>
#include <cmath>
using namespace std;#define MAX_N 10000
#define MAX_M 500000
#define INF 0x3f3f3f3fstruct edge {int to, val, next;
} edg[MAX_M + 5];int head[MAX_N + 5], vis[MAX_N + 5], dis[MAX_N + 5];
int edg_cnt;// 此处采用链式前向星存储有向图
void add_edge(int a, int b, int c) {edg[edg_cnt].to = b;edg[edg_cnt].val = c;edg[edg_cnt].next = head[a];head[a] = edg_cnt++;
}void dijkstra(int n, int s) { // 源点到自身的距离为0dis[s] = 0;for (int i = 1; i < n; i++) {int ind = -1;// 遍历dis数组,找到dis值最小的结点for (int j = 1; j <= n; j++) {if (vis[j]) continue;if (ind == -1 || dis[j] < dis[ind]) ind = j;}// 对于找到的结点,更新与该点的连的点到源点的值for (int j = head[ind]; j != -1; j = edg[j].next) {if (dis[edg[j].to] > dis[ind] + edg[j].val) {dis[edg[j].to] = dis[ind] + edg[j].val;}}// 标注该结点已扩展vis[ind]++;}
}int main() {memset(head, -1, sizeof(head));memset(dis, 0x3f, sizeof(dis));int n, m, s;cin >> n >> m >> s;for (int i = 0; i < m; i++) {int a, b, c;cin >> a >> b >> c;add_edge(a, b, c);}dijkstra(n, s);for (int i = 1; i <= n; i++) {// 采用或运算,当i == 1时,此时表达式为真,cout << " "将不会执行i == 1 || cout << " ";if (dis[i] == INF) {cout << int(pow(2, 31) - 1);} else {cout << dis[i];} } cout << endl;return 0;
}
观察代码可知:算法的时间复杂度为O(n^2)。因为我们要扩展n - 1次,同时每一次扩展,我们都要遍历一遍dis数组,找到dis值最小的结点 (后续遍历以该结点为起点的边所连接的另一端的结点)
虽然使用上述代码可以Accept上面的这道题,但是对于:P4779 【模板】单源最短路径(标准版),由于数据量较大,所有的测试用例都会超时,所以我们需要对算法进行优化改进,降低算法的时间复杂度。
三 时间复杂度优化
1. 优化策略
第二部分我们说到,O(n^2)的算法时间复杂度过大,故需要进行优化。
其实很简单,我们可以借助 优先队列 来优化我们的算法,首先最外面的for循环是不可必避免的,所以我们只能将遍历dis数组这一操作进行优化,我们在优先队列中存储这样的一个二元组<now, dis> 分别表示:当前是几号结点,以及该结点到源点的距离 ,对于优先队列中的元素,我们根据dis的值进行优先级排比,dis越小的二元组在优先队列的头部,这样,我们每次弹出的结点就是距离源点最近的结点,我们也就不再需要线性的遍历dis数组,所以从线性的遍历dis数组O(N),变成了使用有优先队列O(logN) 这里插一句话,优先队列的底层实现是采用二叉树。
所以Dijkstra算法的时间复杂度由之前的O(N^2) 优化为 O(N * logN)
2. 优化后的代码
这里以洛谷4479题为例,题目相关链接上面有。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stack>
#include <algorithm>
#include <string>
#include <map>
#include <cstring>
#include <set>
#include <vector>
using namespace std;#define MAX_N 100000
#define MAX_M 200000/*关于Data类的说明now 表示结点编号,dis表示该结点到源点的距离重载 < ,dis越小,优先级越高
*/
struct Data {int now, dis;Data(int x, int y) : now(x), dis(y) {}bool operator< (const Data &b) const {return this->dis > b.dis;}
};struct edge {int to, val, next;
} edg[MAX_M + 5];int head[MAX_N + 5], vis[MAX_N + 5], dis[MAX_N + 5];
int edg_cnt;void add_edge(int a, int b, int c) {edg[edg_cnt].to = b;edg[edg_cnt].val = c;edg[edg_cnt].next = head[a];head[a] = edg_cnt++;
}void dijkstra(int n, int s) {dis[s] = 0;priority_queue<Data> que;que.push(Data(s, 0));for (int i = 1; i < n && !que.empty(); i++) {int ind = que.top().now;que.pop();while (vis[ind] && !que.empty()) {ind = que.top().now;que.pop();}for (int j = head[ind]; j != -1; j = edg[j].next) {if (dis[edg[j].to] > dis[ind] + edg[j].val) {dis[edg[j].to] = dis[ind] + edg[j].val;// vis[edg[j].to] = 0;que.push(Data(edg[j].to, dis[edg[j].to]));}}vis[ind]++;}
}int main() {memset(head, -1, sizeof(head));memset(dis, 0x3f, sizeof(dis));int n, m, s;cin >> n >> m >> s;for (int i = 0; i < m; i++) {int a, b, c;cin >> a >> b >> c;add_edge(a, b, c);}dijkstra(n, s);for (int i = 1; i <= n; i++) {i == 1 || cout << " ";cout << dis[i];}cout << endl;return 0;
}
大家肯能会询问,在使用优先队列时,为什么获取队首元素之后,还要使用 while 循环,不断进行弹出操作呢?
这是因为此次弹出的点,有可能是之前已经扩展过的点,所以我们舍弃,直接看队列中的下一个结点。
我们现在假设,之前某一时刻以X结点为边的起点进行扩展,假设扩展了2号结点,对应边的权值依次为2,所以向优先队列中压入了<2, 2>,而之后又有一次,以Y结点为边的起点进行扩展,假设也扩展了2号结点,对应边的权值依次为1,所以向优先队列中压入了<2, 1>,显然队列中<2, 1>排在<2, 2>的前面,然后我们假设队列中的其他二元组<a, b>的b都比2大,即优先队列中的前两项为<2, 1>, <2, 2>,弹出<2, 1>后,我们将2号结点标记为1,也就是已经扩展过了的,所以之后的<2, 2>就是没有意义的,我们可以直接从队列中弹出,不做其他操作。
四 结语
图论是数据结构和算法中难度较大的一部分,自己虽然当年数据结构考试拿了一个不错的分数,但是这并不代表自己可以熟练编程实现代码,所以,加油吧!路漫漫其修远兮,吾将上下而求索!
另外,明天好像就出面试结果了。拜托拜托,无量天尊、如来佛祖、还有上帝、真主,保佑我过呀!Wu~~~~~
Dijkstra算法求解单源最短路径问题
文章目录
- 一 前言
- 二 Dijkstra 算法讲解
- 1. 贪心算法的证明
- 2. 算法实现说明
- 3. 初版Dijkstra算法代码
- 三 时间复杂度优化
- 1. 优化策略
- 2. 优化后的代码
- 四 结语
一 前言
上一次的文章更新还是在5月份,现在都快8月份了,整整三个月没有记录学习的点点滴滴。哎,主要是现在自己面临读研的一系列事情,到现在面试结果也没有出来,如果没中,那真的要给老师,给招生办跪下了,呜~~~~~~, 小菜鸡在大佬面前瑟瑟发抖,给我一个读书的机会吧!!!
本次记录的是最短路径算法中的单源最短路经Dijkstra算法,也就是确定图中某一个点,求出图中其他所有点到该点的最短距离分别是多少。
二 Dijkstra 算法讲解
1. 贪心算法的证明
Dijkstra算法的基本思想是贪心思想,关于贪心算法求解问题时,我们需要进行证明两点:
- 最优子结构
- 局部最优性(局部最优解可以得到全局最优解)
关于算法的证明,小编就不多说了,这里直接分享一篇博客:迪杰斯特拉(Dijkstra)算法最通俗易懂的讲解 ,大家要是觉得解释的不是很好,最好还是去算法书上看看 。
2. 算法实现说明
在实现过程中,重要的就是我们引入了两个数组:dis数组和vis数组
dis数组:用来记录每个点到原点的最短路经。
vis数组:用来标记,某个点是否已经扩展过。
我们要做的就是在所有未扩展的点中,选出一个dis值最小的点进行松弛操作,这样n - 1次操作之后,即可知道原点到所有其他点的最短距离。
算法核心操作:
现在假设我们已知结点A到源点S的最短距离为X,也存在从结点A指向结点B的有向边,该边的权值为val,显然结点B到原点会有一个距离,我们假设为Y,若X + val < Y,换句话说就是,我们现在找到了源点S到结点B的更短的一个路径,此时我们更新dis数组中到结点B到源点的距离值。
现在我们模拟一遍算法的执行流程
- 首先初始化dis数组和vis数组,dis数组中源点的值为0,剩余点的dis值为极大值;vis数组中均为0 ,其中0表示没有扩展,1表示该点已被扩展。
- 遍历dis数组,找到值最小,且没有扩展(vis值为0)的点,此时为1号结点。观察图可知,1号结点可以到达2, 3号结点,所以计算可知,结点2到源点的距离为6 + 0 = 6 < dis[2],结点3到源点的距离为0 + 1 = 1 < dis[3], 所以2, 3号结点均可以被更新。至此1号结点已经被扩展,标记vis[1] = 1;
- 遍历dis数组,找到值最小,且没有扩展(vis值为0)的点,此时为3号结点。观察图可知,3号结点可以到达2,4,6号结点,计算可知:2号结点到源点的距离为3 + dis[3] = 3 + 1 = 4(连接3号结点和2号结点的边的权值为3,3号结点到源点的最小距离为1)这个新的距离比原来2号结点到源点的距离6要小,所以我们需要进行距离的更新。同理,我们更新4和6号结点的距离。
- 按照之前的规则,我们相似操作,最终,我们可以得到如下的结果
3. 初版Dijkstra算法代码
我们以洛谷第3371题为例子:P3371 【模板】单源最短路径(弱化版)
解题代码如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stack>
#include <algorithm>
#include <string>
#include <map>
#include <set>
#include <vector>
#include <cstring>
#include <cmath>
using namespace std;#define MAX_N 10000
#define MAX_M 500000
#define INF 0x3f3f3f3fstruct edge {int to, val, next;
} edg[MAX_M + 5];int head[MAX_N + 5], vis[MAX_N + 5], dis[MAX_N + 5];
int edg_cnt;// 此处采用链式前向星存储有向图
void add_edge(int a, int b, int c) {edg[edg_cnt].to = b;edg[edg_cnt].val = c;edg[edg_cnt].next = head[a];head[a] = edg_cnt++;
}void dijkstra(int n, int s) { // 源点到自身的距离为0dis[s] = 0;for (int i = 1; i < n; i++) {int ind = -1;// 遍历dis数组,找到dis值最小的结点for (int j = 1; j <= n; j++) {if (vis[j]) continue;if (ind == -1 || dis[j] < dis[ind]) ind = j;}// 对于找到的结点,更新与该点的连的点到源点的值for (int j = head[ind]; j != -1; j = edg[j].next) {if (dis[edg[j].to] > dis[ind] + edg[j].val) {dis[edg[j].to] = dis[ind] + edg[j].val;}}// 标注该结点已扩展vis[ind]++;}
}int main() {memset(head, -1, sizeof(head));memset(dis, 0x3f, sizeof(dis));int n, m, s;cin >> n >> m >> s;for (int i = 0; i < m; i++) {int a, b, c;cin >> a >> b >> c;add_edge(a, b, c);}dijkstra(n, s);for (int i = 1; i <= n; i++) {// 采用或运算,当i == 1时,此时表达式为真,cout << " "将不会执行i == 1 || cout << " ";if (dis[i] == INF) {cout << int(pow(2, 31) - 1);} else {cout << dis[i];} } cout << endl;return 0;
}
观察代码可知:算法的时间复杂度为O(n^2)。因为我们要扩展n - 1次,同时每一次扩展,我们都要遍历一遍dis数组,找到dis值最小的结点 (后续遍历以该结点为起点的边所连接的另一端的结点)
虽然使用上述代码可以Accept上面的这道题,但是对于:P4779 【模板】单源最短路径(标准版),由于数据量较大,所有的测试用例都会超时,所以我们需要对算法进行优化改进,降低算法的时间复杂度。
三 时间复杂度优化
1. 优化策略
第二部分我们说到,O(n^2)的算法时间复杂度过大,故需要进行优化。
其实很简单,我们可以借助 优先队列 来优化我们的算法,首先最外面的for循环是不可必避免的,所以我们只能将遍历dis数组这一操作进行优化,我们在优先队列中存储这样的一个二元组<now, dis> 分别表示:当前是几号结点,以及该结点到源点的距离 ,对于优先队列中的元素,我们根据dis的值进行优先级排比,dis越小的二元组在优先队列的头部,这样,我们每次弹出的结点就是距离源点最近的结点,我们也就不再需要线性的遍历dis数组,所以从线性的遍历dis数组O(N),变成了使用有优先队列O(logN) 这里插一句话,优先队列的底层实现是采用二叉树。
所以Dijkstra算法的时间复杂度由之前的O(N^2) 优化为 O(N * logN)
2. 优化后的代码
这里以洛谷4479题为例,题目相关链接上面有。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stack>
#include <algorithm>
#include <string>
#include <map>
#include <cstring>
#include <set>
#include <vector>
using namespace std;#define MAX_N 100000
#define MAX_M 200000/*关于Data类的说明now 表示结点编号,dis表示该结点到源点的距离重载 < ,dis越小,优先级越高
*/
struct Data {int now, dis;Data(int x, int y) : now(x), dis(y) {}bool operator< (const Data &b) const {return this->dis > b.dis;}
};struct edge {int to, val, next;
} edg[MAX_M + 5];int head[MAX_N + 5], vis[MAX_N + 5], dis[MAX_N + 5];
int edg_cnt;void add_edge(int a, int b, int c) {edg[edg_cnt].to = b;edg[edg_cnt].val = c;edg[edg_cnt].next = head[a];head[a] = edg_cnt++;
}void dijkstra(int n, int s) {dis[s] = 0;priority_queue<Data> que;que.push(Data(s, 0));for (int i = 1; i < n && !que.empty(); i++) {int ind = que.top().now;que.pop();while (vis[ind] && !que.empty()) {ind = que.top().now;que.pop();}for (int j = head[ind]; j != -1; j = edg[j].next) {if (dis[edg[j].to] > dis[ind] + edg[j].val) {dis[edg[j].to] = dis[ind] + edg[j].val;// vis[edg[j].to] = 0;que.push(Data(edg[j].to, dis[edg[j].to]));}}vis[ind]++;}
}int main() {memset(head, -1, sizeof(head));memset(dis, 0x3f, sizeof(dis));int n, m, s;cin >> n >> m >> s;for (int i = 0; i < m; i++) {int a, b, c;cin >> a >> b >> c;add_edge(a, b, c);}dijkstra(n, s);for (int i = 1; i <= n; i++) {i == 1 || cout << " ";cout << dis[i];}cout << endl;return 0;
}
大家肯能会询问,在使用优先队列时,为什么获取队首元素之后,还要使用 while 循环,不断进行弹出操作呢?
这是因为此次弹出的点,有可能是之前已经扩展过的点,所以我们舍弃,直接看队列中的下一个结点。
我们现在假设,之前某一时刻以X结点为边的起点进行扩展,假设扩展了2号结点,对应边的权值依次为2,所以向优先队列中压入了<2, 2>,而之后又有一次,以Y结点为边的起点进行扩展,假设也扩展了2号结点,对应边的权值依次为1,所以向优先队列中压入了<2, 1>,显然队列中<2, 1>排在<2, 2>的前面,然后我们假设队列中的其他二元组<a, b>的b都比2大,即优先队列中的前两项为<2, 1>, <2, 2>,弹出<2, 1>后,我们将2号结点标记为1,也就是已经扩展过了的,所以之后的<2, 2>就是没有意义的,我们可以直接从队列中弹出,不做其他操作。
四 结语
图论是数据结构和算法中难度较大的一部分,自己虽然当年数据结构考试拿了一个不错的分数,但是这并不代表自己可以熟练编程实现代码,所以,加油吧!路漫漫其修远兮,吾将上下而求索!
另外,明天好像就出面试结果了。拜托拜托,无量天尊、如来佛祖、还有上帝、真主,保佑我过呀!Wu~~~~~