最短路
定义¶
(还记得这些定义吗?在阅读下列内容之前,请务必了解图论基础部分。)
- 路径
- 最短路
- 有向图中的最短路、无向图中的最短路
- 单源最短路、每对结点之间的最短路
性质¶
对于边权为正的图,任意两个结点之间的最短路,不会经过重复的结点。
对于边权为正的图,任意两个结点之间的最短路,不会经过重复的边。
对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过
Floyd 算法¶
是用来求任意两个结点之间的最短路的。
复杂度比较高,但是常数小,容易实现。(我会说只有三个 for
吗?)
适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)
实现¶
我们定义一个数组 f[k][x][y]
,表示只允许经过结点
很显然, f[n][x][y]
就是结点
我们来考虑怎么求这个数组
f[0][x][y]
:边权,或者 f[0][x][x]
什么时候应该是
f[k][x][y] = min(f[k-1][x][y], f[k-1][x][k]+f[k-1][k][y])
上面两行都显然是对的,然而这个做法空间是
但我们发现数组的第一维是没有用的,于是可以直接改成 f[x][y] = min(f[x][y], f[x][k]+f[k][y])
,
1 2 3 4 5 6 7 | for (k = 1; k <= n; k++) { for (i = 1; i <= n; i++) { for (j = 1; j <= n; j++) { f[i][j] = min(f[i][j], f[i][k] + f[k][j]); } } } |
时间复杂度是
应用¶
给一个正权无向图,找一个最小权值和的环。
首先这一定是一个简单环。
想一想这个环是怎么构成的。
考虑环上编号最大的结点 u。
f[u-1][x][y]
和 (u,x), (u,y)共同构成了环。
在Floyd的过程中枚举u,计算这个和的最小值即可。
已知一个有向图中任意两点之间是否有连边,要求判断任意两点是否连通。
该问题即是求图的传递闭包。
我们只需要按照 Floyd 的过程,逐个加入点判断一下。
只是此时的边的边权变为
再进一步用 bitset 优化,复杂度可以到
1 2 3 4 | //std::bitset<SIZE> f[SIZE]; for (k = 1; k <= n; k++) for (i = 1; i <= n; i++) if(f[i][k]) f[i] = f[i] & f[k]; |
Bellman-Ford 算法¶
一种基于松弛(relax)操作的最短路算法。
支持负权。
能找到某个结点出发到所有结点的最短路,或者报告某些最短路不存在。
在国内 OI 界,你可能听说过的“SPFA”,就是 Bellman-Ford 算法的一种实现。(优化)
实现¶
假设结点为
先定义
三角形不等式:
证明:反证法,如果不满足,那么可以用
Bellman-Ford 算法如下:
1 | while (1) for each edge(u, v) relax(u, v); |
当一次循环中没有
每次循环是
答案是
但是此时某些结点的最短路不存在。
我们考虑最短路存在的时候。
由于一次
所以最多(连续)
所以最多循环
总时间复杂度
1 2 3 4 5 6 7 8 9 10 11 | relax(u, v) { dist[v] = min(dist[v], dist[u] + edge_len(u, v)); } for (i = 1; i <= n; i++) { dist[i] = edge_len(S, i); } for (i = 1; i < n; i++) { for each edge(u, v) { relax(u, v); } } |
注:这里的
应用¶
给一张有向图,问是否存在负权环。
做法很简单,跑 Bellman-Ford 算法,如果有个点被
如果
队列优化:SPFA¶
即 Shortest Path Faster Algorithm。
很多时候我们并不需要那么多无用的
很显然,只有上一次被
那么我们用队列来维护“哪些结点可能会引起
1 2 3 4 5 6 7 8 9 10 11 12 13 | q = new queue(); q.push(S); in_queue[S] = true; while (!q.empty()) { u = q.pop(); in_queue[u] = false; for each edge(u, v) { if (relax(u, v) && !in_queue[v]) { q.push(v); in_queue[v] = true; } } } |
SPFA 的时间复杂度为
SPFA 的优化之 SLF¶
即 Small Label First。
即在新元素加入队列时,如果队首元素权值大于新元素权值,那么就把新元素加入队首,否则依然加入队尾。
该优化在确实在一些图上有显著效果,其复杂度也有保证,但是如果有负权边的话,可以直接卡到指数级。
Dijkstra 算法¶
Dijkstra 是个人名(荷兰姓氏)。
IPA:/ˈdikstrɑ/或/ˈdɛikstrɑ/。
这种算法只适用于非负权图,但是时间复杂度非常优秀。
也是用来求单源最短路径的算法。
实现¶
主要思想是,将结点分成两个集合:已确定最短路长度的,未确定的。
一开始第一个集合里只有
然后重复这些操作:
(1)
(2)从第二个集合中,选取一个最短路长度最小的结点,移到第一个集合中。
直到第二个集合为空,算法结束。
时间复杂度:只用分析集合操作, delete-min
, decrease-key
。
如果用暴力:
如果用堆
如果用 priority_queue:
(注:如果使用 priority_queue,无法删除某一个旧的结点,只能插入一个权值更小的编号相同结点,这样操作导致堆中元素是
如果用线段树(ZKW 线段树):
如果用 Fibonacci 堆:
等等,还没说正确性呢!
分两步证明:先证明任何时候第一个集合中的元素的
再证明第一个集合中的元素的最短路已经确定。
第一步,一开始时成立(基础),在每一步中,加入集合的元素一定是最大值,且是另一边最小值,
第二步,考虑每次加进来的结点,到他的最短路,上一步必然是第一个集合中的元素(否则他不会是第二个集合中的最小值,而且有第一步的性质),又因为第一个集合已经全部
1 2 3 4 5 6 7 8 9 10 11 | H = new heap(); H.insert(S, 0); dist[S] = 0; for (i = 1; i <= n; i++) { u = H.delete_min(); for each edge(u, v) { if (relax(u, v)) { H.decrease_key(v, dist[v]); } } } |
不同方法的比较¶
Floyd | Bellman-Ford | Dijkstra |
---|---|---|
每对结点之间的最短路 | 单源最短路 | 单源最短路 |
没有负环的图 | 任意图 | 非负权图 |
输出方案¶
开一个 pre
数组,在更新距离的时候记录下来后面的点是如何转移过去的,算法结束前再递归地输出路径即可。
比如 Floyd 就要记录 pre[i][j] = k;
,Bellman-Ford 和 Dijkstra 一般记录 pre[v] = u
。
拓展:分层图最短路¶
分层图最短路,一般模型为有
其中,
事实上,这个 DP 就相当于把每个结点拆分成了
模板题:[JLOI2011]飞行路线¶
题意:有一个
参考核心代码:
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 38 39 40 41 42 43 44 45 | struct State { // 优先队列的结点结构体 int v, w, cnt; // cnt 表示已经使用多少次免费通行权限 State() {} State(int v, int w, int cnt) : v(v), w(w), cnt(cnt) {} bool operator<(const State &rhs) const { return w > rhs.w; } }; void dijkstra() { memset(dis, 0x3f, sizeof dis); dis[s][0] = 0; pq.push(State(s, 0, 0)); // 到起点不需要使用免费通行权,距离为零 while (!pq.empty()) { const State top = pq.top(); pq.pop(); int u = top.v, nowCnt = top.cnt; if (done[u][nowCnt]) continue; done[u][nowCnt] = true; for (int i = head[u]; i; i = edge[i].next) { int v = edge[i].v, w = edge[i].w; if (nowCnt < k && dis[v][nowCnt + 1] > dis[u][nowCnt]) { // 可以免费通行 dis[v][nowCnt + 1] = dis[u][nowCnt]; pq.push(State(v, dis[v][nowCnt + 1], nowCnt + 1)); } if (dis[v][nowCnt] > dis[u][nowCnt] + w) { // 不可以免费通行 dis[v][nowCnt] = dis[u][nowCnt] + w; pq.push(State(v, dis[v][nowCnt], nowCnt)); } } } } int main() { n = read(), m = read(), k = read(); // 笔者习惯从 1 到 n 编号,而这道题是从 0 到 n - 1,所以要处理一下 s = read() + 1, t = read() + 1; while (m--) { int u = read() + 1, v = read() + 1, w = read(); add(u, v, w), add(v, u, w); // 这道题是双向边 } dijkstra(); int ans = std::numeric_limits<int>::max(); // ans 取 int 最大值为初值 for (int i = 0; i <= k; ++i) ans = std::min(ans, dis[t][i]); // 对到达终点的所有情况取最优值 println(ans); } |
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用