『学习笔记』最短路和差分约束系统
最短路
Floyd 多源最短路径
引入
Floyd 利用了 dp 的思想,主要可以用来求任意两个节点的连通性或最短路,可以有负边权,但不能有负环。
例题
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}=+\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;
}
Bellman-Ford 单源最短路径
引入
Bellman-Ford 算法是一种基于松弛操作的单源最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
过程
对于边 \((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;
}
SPFA 单源最短路径
在 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;
}
Dijkstra 单源最短路径
引入
Dijkstra 是一种在没有非负边权的图上求单源最短路径的算法,基于贪心的思想,所以不能有非负边权。
过程
将结点分成两个集合:已确定最短路长度的点集(记为S集合)的和未确定最短路长度的点集(记为 \(T\) 集合)。一开始所有的点都属于 \(T\) 集合。然后重复这些操作:从 \(T\) 集合中,选取一个最短路长度最小的结点,移到 \(S\) 集合中。对那些刚刚被加入 \(S\) 集合的结点的所有出边执行松弛操作。直到 \(T\) 集合为空,算法结束。
例题
方法一:朴素做法
不加任何优化,时间复杂度 \(\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;
}
}
}
}
方法二:手写二叉堆或优先队列
若用优先队列,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数是 \(\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});
}
}
}
}
}
总结
| 算法 | 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)\) |
差分约束系统
引入
给定 \(m\) 个形如 \(x_i-x_j\le c_k\)(\(x_i\) 为变量,\(c_k\) 为常数)的一次不等式约束条件,求一组 \(\{x_i\}\) 使得满足所有约束条件。
过程
每个不等式可转换为 \(x_i\le x_j+c_k\),类似于最短路中的不等式 \(dis_v\le dis_u+w\),所以可以理解成在图上有一条从 \(j\) 到 \(i\)、长度为 \(c_k\) 的边。
我们建一个到每个节点都有边的超级原点 \(0\),保证图联通,用 SPFA 计算出 \(0\) 到每个节点的最短路,如果存在负环,则无解,否则 \(\{x_i\}=\{dis_i\}\)。
例题
spoj116 Intervals
板子题,但除了原本要建的边,还需要再建所有 \(i-1\xrightarrow{0}i\) 和 \(i+1\xrightarrow{-1}i\)。
代码
#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;
}

浙公网安备 33010602011771号