『学习笔记』最短路和差分约束系统

1|0最短路

1|1Floyd 多源最短路径

1|0引入

Floyd 利用了 dp 的思想,主要可以用来求任意两个节点的连通性或最短路,可以有负边权,但不能有负环

1|0例题

luogu B3647 【模板】Floyd
我们设 \(f_{k,i,j}\) 为除了 \(i\)\(j\) 外只经过前 \(k\) 个结点,从 \(i\)\(j\) 的最短路,\(w_{i,j}\) 表示初始时给我们的从 \(i\)\(j\)的边权。显然 \(f_{0,i,j}=w_{i,j}\)。 那么当加⼊了⼀个顶点 \(k\) 之后,最短路如果有变化的话⼀定是以 \(k\) 为中间顶点,那么可以得到 \(f_{k,i,j}=\min\{f_{k-1,i,j},f_{k−1,i,k}+f_{k−1,k,j}\}\)。同时,可以利用滚动数组优化掉第一维,因为,首先在利用中间节点k更新距离的时候,\(f_{i,k}\)\(f_{k,j}\) 的值肯定不会被更新,也就是k作为端点的情况,值不会发生改变。其次,即使我们遇到 \(k=j\) 的情况 \(f_{i,j}=\min\{f_{i,j},f_{i,j}+f_{j,j}\}\),此处利用 \(f_{i,j}\) 递推 \(f_{i,j}\) 不会改变 \(f_{i,j}\)
所以第一重循环枚举中间节点 \(k\),第二重、第三重分别枚举 \(i,j\),转移方程是:

\[f_{i,j}=\min\{f_{i,j},f_{i,k}+f_{k,j}\} \]

由于求的是最小值,所以边界是 \(f_{i,j}=+\infty,f_{i,i}=0\)

点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll n, m, dis[4005][4005], u, v, w, ans = 0; inline ll read() { ll x = 0, f = 1; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') { f = -1; } ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return x * f; } inline void write(ll x) { if(x < 0) { putchar('-'); x = -x; } if(x > 9) { write(x / 10); } putchar(x % 10 + '0'); } int main() { n = read(), m = read(); memset(dis, 0x3f, sizeof(dis)); while(m--) { u = read(), v = read(), w = read(); dis[v][u] = dis[u][v] = min(dis[u][v], w); } for(int i = 1; i <= n; i++) { dis[i][i] = 0; } for(int x = 1; x <= n; x++) { for(int i = 1; i <= n; i++) { for(int j = 1; j <= n; j++) { dis[i][j] = min(dis[i][x] + dis[x][j], dis[i][j]); } } } for(int i = 1; i <= n; i++) { for(int j = 1; j <= n; j++) { cout << dis[i][j] << ' '; } cout << '\n'; } return 0; }

1|2Bellman-Ford 单源最短路径

1|0引入

Bellman-Ford 算法是一种基于松弛操作的单源最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

1|0过程

对于边 \((u,v)\),松弛操作对应的式子:\(dis(v)=min(dis(v),dis(u)+w(u,v))\)。我们尝试用 \(S\to u \to v\)(其中\(S \to u\) 的路径取最短路)这条路径去更新 \(v\) 节点最短路的长度,如果这条路径更优,就进行更新。Bellman-Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。每次循环是 \(\mathcal{O}(m)\) 的,那么最多会循环多少次呢?在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少加 \(1\),而最短路的边数最多为 \(n-1\),因此整个算法最多执行 \(n-1\) 轮松弛操作。故总时间复杂度为 \(\mathcal{O}(n\times m)\)。但还有一种情况,如果从 \(S\) 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n-1\) 轮,因此如果第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发,能够抵达一个负环。需要注意的是,以 \(S\) 点为源点跑 Bellman-Ford 算法时,如果没有给出存在负环的结果,只能说明从 \(S\) 点出发不能抵达一个负环,而不能说明图上不存在负环。因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 \(0\) 的边,然后以超级源点为起点执行 Bellman-Ford 算法。

点击查看 Bellman-Ford 判负环的代码
inline bool bellmanford(int n, int s) { memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; bool flag = 0; for(int i = 1; i <= n; i++) { flag = 0; for(int u = 1; u <= n; u++) { if(dis[u] == 1e9) { continue; } for(auto ed : e[u]) { int v = ed.v, w = ed.w; if(dis[v] > dis[u] + w) { dis[v] = dis[u] + w; flag = 1; } } } if(!flag) { break; } } return flag; }

1|3SPFA 单源最短路径

在 Bellman-Ford 的基础上加一个队列进行优化。很多时候我们并不需要那么多无用的松弛操作。很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。同样也是松弛 \(n-1\) 轮即可,所以 SPFA 最坏情况下会退化成 Bellman-Ford。

点击查看 SPFA 判负环代码
inline bool spfa() { queue<int> q; for(int i = 1; i <= n; i++) { dis[i] = INT_MAX; vis[i] = 0; } q.push(1); dis[1] = 0; vis[1] = 1; ++cnt[1]; while(!q.empty()) { int u = q.front(); q.pop(); vis[u] = 0; for(int i = h[u]; i; i = e[i].ne) { int v = e[i].to; if(dis[v] > dis[u] + e[i].dis) { dis[v] = dis[u] + e[i].dis; if(!vis[v]) { vis[v] = 1; q.push(v); ++cnt[v]; if(cnt[v] > n) { return 1; } } } } } return 0; }

1|4Dijkstra 单源最短路径

1|0引入

Dijkstra 是一种在没有非负边权的图上求单源最短路径的算法,基于贪心的思想,所以不能有非负边权。

1|0过程

将结点分成两个集合:已确定最短路长度的点集(记为S集合)的和未确定最短路长度的点集(记为 \(T\) 集合)。一开始所有的点都属于 \(T\) 集合。然后重复这些操作:从 \(T\) 集合中,选取一个最短路长度最小的结点,移到 \(S\) 集合中。对那些刚刚被加入 \(S\) 集合的结点的所有出边执行松弛操作。直到 \(T\) 集合为空,算法结束。

1|0例题

luogu P4779 【模板】单源最短路径(标准版)

1|0方法一:朴素做法

不加任何优化,时间复杂度 \(\mathcal{O}(n^2+m)=\mathcal{O}(n^2)\),显然过不了,代码如下:

inline void dijkstra(int n, int s) { memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; for(int i = 1; i <= n; i++) { int u = 0, mini = 0x3f3f3f3f; for(int j = 1; j <= n; j++) { if(!vis[j] && dis[j] < mini) { u = j; mini = dis[j]; } } vis[u] = 1; for(auto ed : e[u]) { int v = ed.v, w = ed.w; if(dis[v] > dis[u] + w) { dis[v] = dis[u] + w; } } } }

1|0方法二:手写二叉堆或优先队列

若用优先队列,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数是 \(\mathcal{O}(m)\) 的,时间复杂度为 \(\mathcal{O}(m\times\log_2 m)\)
二叉堆的时间复杂度是 \(\mathcal{O}(m\times\log_2 n)\),稍微比优先队列快一点,但优先队列代码好写亿点。

点击查看代码
inline void dijkstra(int s) { dis[s] = 0; q.push({0, s}); while(!q.empty()) { node tmp = q.top(); q.pop(); int x = tmp.pos, d = tmp.dis; if(vis[x]) { continue; } vis[x] = 1; for(int i = h[x]; i; i = e[i].next) { int y = e[i].to; if(dis[y] > dis[x] + e[i].dis) { dis[y] = dis[x] + e[i].dis; if(!vis[y]) { q.push({dis[y], y}); } } } } }

1|5总结

算法 Floyd Bellman-Ford or SPFA Dijkstra
类型 多源 单源 单源
图的条件 无负环 非负边权
检测负环 不能
时间复杂度(全源) \(\mathcal{O}(n^3)\) \(\mathcal{O}(n^2\times m)\) \(\mathcal{O}\left(n\times(n^2+m)\right)\)\(\mathcal{O}(n\times m\times \log_2 m)\)

2|0差分约束系统

2|1引入

给定 \(m\) 个形如 \(x_i-x_j\le c_k\)\(x_i\) 为变量,\(c_k\) 为常数)的一次不等式约束条件,求一组 \(\{x_i\}\) 使得满足所有约束条件。

2|2过程

每个不等式可转换为 \(x_i\le x_j+c_k\),类似于最短路中的不等式 \(dis_v\le dis_u+w\),所以可以理解成在图上有一条从 \(j\)\(i\)、长度为 \(c_k\) 的边。
我们建一个到每个节点都有边的超级原点 \(0\),保证图联通,用 SPFA 计算出 \(0\) 到每个节点的最短路,如果存在负环,则无解,否则 \(\{x_i\}=\{dis_i\}\)

2|3例题

spoj116 Intervals
板子题,但除了原本要建的边,还需要再建所有 \(i-1\xrightarrow{0}i\)\(i+1\xrightarrow{-1}i\)

1|0代码

#include<bits/stdc++.h> using namespace std; const int N = 5e5 + 5; struct node { int v, w, n; } e[N]; int maxi = 0, hd[N], tot; int dis[N], vis[N], ti[N]; deque<int> deq; inline void add(int x, int y, int z) { e[++tot].v = y; e[tot].w = z; e[tot].n = hd[x]; hd[x] = tot; } inline int SPFA(int st) { dis[st] = 0; deq.push_back(st); vis[st] = 1; while(!deq.empty()) { int x = deq.front(); deq.pop_front(); vis[x] = 0; ti[x]++; if(ti[x] > maxi) { return 1; } for(int i = hd[x]; i; i = e[i].n) { int y = e[i].v, z = e[i].w; if(dis[x] + z > dis[y]) { dis[y] = dis[x] + z; if(!vis[y]) { vis[y] = 1; if(!deq.empty() && dis[y] > dis[deq.front()]) { deq.push_front(y); } else { deq.push_back(y); } } } } } return dis[maxi]; } int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); int t; cin >> t; while(t--) { memset(hd, 0, sizeof(hd)); tot = maxi = 0; for(int i = 0; i < N; i++) { dis[i] = -1e9; } memset(ti, 0, sizeof(ti)); int n; cin >> n; for(int i = 1; i <= n; i++) { int x, y, z; cin >> x >> y >> z; maxi = max(maxi, max(x, y)); add(x - 1, y, z); } for(int i = 0; i <= maxi; i++) { if(i != 0) { add(i - 1, i, 0); } if(i != maxi) { add(i + 1, i, -1); } } for(int i = 1; i <= maxi; i++) { add(0, i, 0); } cout << SPFA(0) << "\n"; } return 0; }

__EOF__

本文作者cyf1208
本文链接https://www.cnblogs.com/cyf1208/p/17755014.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyf1208  阅读(72)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示