五种最短路的解决方法 1、Dijkstra算法: 时间复杂度: 朴素版 O(n^2),用于无负权边
核心思路:
每次找到集合中距离最短的边进行更新,并用这条确定的最短的边去扩展与他相连通的所有点, 并更新距离,用st[]数组标记已经找到了最短距离。 稀疏图用邻接矩阵存,反之用邻接表存。
注意:
(1)邻接矩阵存边时,可以用g[a][b] = min(g[a][b], c),避免重边和自环,但也要先初始化memset(g,0x3f, sizeof g);
代码模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 1 ; i <= n; i ++) { int t = -1 ; for (int j = 1 ; j <= n; j ++) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; for (int j = 1 ; j <= n; j ++) dist[j] = min (dist[j], dist[t] + g[t][j]); st[t] = true ; } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; }
2、堆优化版Dijkstra算法: 时间复杂度:
O(mlogn),用于无负权边。
核心思路:
对朴素版 Dijkstra算法的核心进行优化,就是每次取出的是所有边中最短的边, 我们可以利用小根堆去维护需要遍历的所有边,这样每次取出堆顶元素,即最短的边了。
注意:
(1)pair类型的优先队列是按照第一个关键词进行排序的,故第一位只能放距离。 (2)邻接表初始化表头memset(h, -1, sizeof h); (3)初始化距离数组dist为 INF,便于寻找最短路径。
代码模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push ({0 , 1 }); while (heap.size ()) { auto t = heap.top (); heap.pop (); int ver = t.second, distance = t.first; if (st[ver]) continue ; st[ver] = true ; for (int i = h[ver]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > dist[ver] + w[i]) { dist[j] = dist[ver] + w[i]; heap.push ({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; }
3、Bellman-ford算法: 时间复杂度: O(nm),用于有数限制的最短路问题。 与其他最短路算法不同是,Bellman-ford算法需要自己定义一个结构体,进行存边。
核心思路:
(1)初始化dist数组,并初始化起点的距离 (2)迭代k次,表示只能经过k条边 (3)迭代所有边,即迭代m次,每次迭代前备份一下上一次的dist数组(last),用于后续更新dist距离, (4)第m次时迭代更新与起点距离m条边及以内的点的最短距离(松弛)
注意:
(1)if (dist[n] > 0x3f3f3f3f / 2) return 0,因为存在负权边。 (2)每次都遍历会遍历所有边,有很多边是无效的,spfa算法针对这一点进行优化。
代码模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < k; i ++) { memcpy (last, dist, sizeof dist); for (int j = 0 ; j < m; j ++) { auto e = edges[j]; dist[e.b] = min (dist[e.b], last[e.a] + e.c); } } if (dist[n] > 0x3f3f3f3f / 2 ) return 0 ; return dist[n]; }
4、Spfa算法: 时间复杂度:
O(m) 是对bellman_ford算法中松弛一步的优化。 不能含有负权回路。
思路:
(1)初始化dist数组 (2)定义一个循环队列,将需要更新最短距离的点加入队列中,这样就不用每次去寻找需要更新的边了 (3)遍历队列中的元素,只要该元素所在的连通块中,存在新的最短距离,就将其加入队列中,去更新后面的元素距离。
注意:
(1)这里的st[]数组意义和Dijkstra算法不同, Dijkstra算法中st[]数组表示是否求出最短距离; Spfa算法中st[]数组表示某个点是否在队列中。 (2)Spfa算法能求含负权边的原因,也是因为(1)中的特性,若存在负权边,就会去重新更新与该点连接的边的最短距离。 (3)Spfa最坏情况是O(nm) (4)循环队列写法:
hh = 0, tt = 0, q[tt ++] = 1; while (hh != tt); int t = q[hh++]; if (tt == N) tt = 0; … q[tt ++] = x; if (tt == N) tt = 0;
(5)spfa算法最后判断的条件是dist[n] == INF的原因是,bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但spfa算法不一样,他相当于采用BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
代码模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 int spfa () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; queue<int > q; q.push (1 ); st[1 ] = true ; while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) { q.push (j); st[j] = true ; } } } } return dist[n]; }
5、spfa算法判负环: 时间复杂度:
O(m)
核心思想:
若图中存在负环,那么spfa算法就会一直不断地更新最短距离,故只要我们发现更新次数 >= n次, 就说明有负环,因为n个点只需要n - 1条边连接,故只需要更新n - 1次。 故我们只需要在每次更新最短距离时,更新cnt[j] = cnt[t] + 1即可,最后若更新次数 >= n次,就结束循环,返回true。 注意: (1)这里需要注意,可能负环与起点1,不连通,故需要将所有点都加入到队列中,然后开始spfa算法遍历即可。
代码模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int spfa () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; queue<int > q; for (int i = 1 ; i <= n; i ++) { q.push (i); st[i] = true ; } while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1 ; if (cnt[t] > n) return true ; if (!st[j]) { q.push (j); st[j] = true ; } } } } return false ; }
6、Floyd算法: 时间复杂度:
O(n^3) 多源最短路问题。
核心思路:
基于DP: (1)状态表示:f[k][i][j] 所有从i出发,最终走到j,且中间只经过节点编号不超过k的所有路径。 (2)属性:min (3)状态计算: 以第i个点是否在路径中进行划分: 所有不包含节点k的路径:d[k - 1][i][j] 所有包含节点k的路径:d[k - 1][i][k] + d[k - 1][k][j] (4)由于每次用的都是上一层的d,故可以把k层优化掉 即 d[i][j] = min(d[i][j], d[i][k] + d[k][j])
注意:
(1)初始化:自环设置成d[i][j] = 0,其他情况设置成INF,便于求min (2)用邻接矩阵存边,直接d[a][b] = min(d[a][b], c);
代码模板: 1 2 3 4 5 6 7 8 9 初始化: (1 )i != j,d[i][j] = 0x3f3f3f3f ,无穷,便于后续求最短路 (2 )i == j,d[i][j] = 0 ,自环 实现: for (int k = 1 ; k <= n; k ++ ) for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= n; j ++ ) d[i][j] = min (d[i][j], d[i][k] + d[k][j]);