NC158 单源最短路
单源最短路是图论算法里非常重要的基础算法,似乎没有之一。。。。。。
常用的算法有dijkstra算法和SPFA算法,下面分别介绍。
题意
给你一个有向图,n点m边,有重边无自环,权值为正,求 1 到 n 的最短路,如果不存在,输出-1.
1. 迪杰斯特拉
这个似乎是数据结构课的新人杀手了。。。。
dijkstra算法的流程如下:
- 初始化dist[1]=0,其他为inf,令s集合表示已经确定最短距离的点
- 遍历n次:
- 寻找不在s中的,距离s集合最近的点t
vis[t]=true
, 即加入s中- 遍历s中的所有点,对每个点j更新
dist[j]=min(dist[j],dist[t]+f[t][j])
,实际这一步可以遍历全部点,因为dij算法不支持负边,无穷大加正数还是无穷大。
这样做,时间复杂度会达到,因为有两重的遍历,通常的题目是稀疏图(例如边数1e6),且点数达到1e5,肯定是不行的。
上述过程中的第3步,我们发现,加入t之后,用t更新s中的所有点是一个线性遍历的过程,但是,我们可以不用线性扫点,而是直接挑选距离S最近的点进行更新,所以可以维护一个小根堆,每次从堆顶取的就是不在S中距离起点最近的那个。
class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param n int 顶点数 * @param m int 边数 * @param graph intvector<vector<>> 一维3个数据,表示顶点到另外一个顶点的边长度是多少 * @return int */ using pii = pair<int, int>; // 定义小根堆,存储当前距离1号点的距离和下标 // 默认的priority_queue是大根堆,要特殊定义一下,这样写是固定写法。 // stl中,pair的比较是先比第一个,再比第二个,所以距离要放前面 using minheap = priority_queue<pii,vector<pii>,greater<pii> >; static const int N = 210, M = 510; // 本题大约的数据范围是200个点,500个边。 // 定义初始距离为无穷大, 这个数约为1e9,够大,且相加不爆int,相乘不爆ll,是算法题中常见的无穷大定义 static const int INF = 0x3f3f3f3f; // 前向星存储 struct Edge { int to, next, dis; } e[M]; int head[N], dist[N], idx = 1; bool vis[N]; void add(int a, int b, int c) { e[idx].to = b; e[idx].next = head[a]; e[idx].dis = c; head[a] = idx++; } void dijkstra(int start) { memset(dist, 0x3f, sizeof dist); dist[start] = 0; minheap h; h.push({0, start}); //先把起点放进去 while (!h.empty()) { // 找到不在S中,距离起点最近的那个 int u = h.top().second; h.pop(); if (!vis[u]) { // 加入S中 vis[u] = true; // 遍历出边 for (int i = head[u]; ~i; i = e[i].next) { int j = e[i].to; // 如果可以更新,就放进小根堆 if (dist[j] > dist[u] + e[i].dis) { dist[j] = dist[u] + e[i].dis; h.push({dist[j], j}); } } } } } int findShortestPath(int n, int m, vector<vector<int> >& graph) { memset(head, -1, sizeof head); memset(vis, 0, sizeof vis); for (auto g : graph) { add(g[0], g[1], g[2]); } dijkstra(1); if (dist[n] == INF) return -1; return dist[n]; } };
- 时间复杂度:, 宽搜部分按边遍历,堆中存的是点,入堆过程的复杂度是.
- 空间复杂度:. 堆中最多存了整个图的全部点。
2. spfa
方法一虽然经典,但是不支持负权边。下面介绍一种支持负权边的算法————SPFA(Shortest Path Faster Algorithm)算法。
算法流程如下:
- 维护一个队列,初始时加入起点
- 每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断dist[v]+len是否小于dist[u],若小于则缩小dist[u](这个操作叫松弛)
- 如果u没有访问过,则u入队
- 直到队列为空。
以样例为例,图解算法流程如下:
- 初始状态队列中为1,dist[i]=0, 其他为inf
- 1出队,2,4入队,更新dist[2]=2, dist[4]=5
- 2,4出队,3,5入队,更新dist[3]=5, dist[5]=10
- 3出队,更新dist[5]=9
- 5出队,队列为空
class Solution { public: static const int N = 210, M = 510; // 本题大约的数据范围是200个点,500个边。 static const int INF = 0x3f3f3f3f; // 定义初始距离为无穷大 struct Edge { int to, next, dis; // 存储图 } e[M]; int head[N], dist[N], idx = 1; bool vis[N]; void add(int a, int b, int c) { e[idx].to = b; e[idx].next = head[a]; e[idx].dis = c; head[a] = idx++; } void spfa(int start) { memset(dist, 0x3f, sizeof dist); dist[start] = 0; queue<int> q; q.push(start); vis[start] = true; //判重数组, 队列中有重复的点没有意义 while (q.size()) { int t = q.front(); q.pop(); vis[t] = false; // 遍历队头的点t的所有出边 for (int i = head[t]; ~i; i = e[i].next) { int j = e[i].to; // t->j的有向边 // 松弛操作j点 if (dist[j] > dist[t] + e[i].dis) { dist[j] = dist[t] + e[i].dis; // 如果j没入队过,把j加入队列 if (!vis[j]) { q.push(j); vis[j] = true; } } } } } int findShortestPath(int n, int m, vector<vector<int> >& graph) { memset(head, -1, sizeof head); memset(vis, 0, sizeof vis); // 邻接矩阵转换成链表形式存图 for (auto g : graph) { add(g[0], g[1], g[2]); } spfa(1); if (dist[n] == INF) return -1; // 如果没有更新到n点,说明不存在最短路径 return dist[n]; } };
- 时间复杂度:通常认为是, m是边数,k是较大的常数,在特殊的数据下,最坏为, 其中n是点数。
- 空间复杂度:. 定义了长度为点数的队列。
3. 总结
选择算法时,需要注意:
- 如果有负权边,必须使用spfa
- 如果没有负权边,对稀疏图建议使用前向星存储+堆优化dijkstra,对稠密图(边数是点数平方级别)的话,如果用堆优化版本,时间复杂度就变成了, 反而劣于普通dij。因此稠密图建议用邻接矩阵存储的dij。当然,用spfa也可以,但记住这玩意在笔试中,如果题目条件给出边权为正,很可能会被出题人故意构造数据卡掉!
- 如果有负环,则最短路是负无穷大,判断方法是在spfa里增加一个数组记录下每个点的最短路被更新过几次,如果超过了n次,说明走到这个点的路径长度超过n了,这就证明在转圈,所以存在负环。