Appearance
Dijkstra
Dijkstra 只适用于边权非负的图。距离和中间加法应使用 64 位整数;下方取 INF=2^62,并假设单条边权与所有需要输出的有限最短路都落在 (-INF,INF) 内。优先队列必须按距离从小到大弹出。
点击展开代码
cpp
vector<long long> dijkstra(int s, int n) {
const long long INF = 1LL << 62;
vector<long long> dis(n + 1, INF);
using pli = pair<long long, int>;
priority_queue<pli, vector<pli>, greater<pli>> q;
dis[s] = 0;
q.push({0, s});
while (q.size()) {
auto [cur, now] = q.top();
q.pop();
if (cur != dis[now])
continue;
for (auto [v, w] : p[now]) {
if (w > INF - dis[now])
continue;
long long nxt = dis[now] + w;
if (dis[v] > nxt) {
dis[v] = nxt;
q.push({nxt, v});
}
}
}
return dis;
}稠密图上,
点击展开代码
cpp
vector<long long> dijkstra(int s, int n) {
const long long INF = 1LL << 62;
vector<long long> dis(n + 1, INF);
vector<bool> vis(n + 1);
dis[s] = 0;
for (int _ = 1; _ <= n; _++) {
int mini = 0;
for (int i = 1; i <= n; i++) {
if (!vis[i] && (!mini || dis[i] < dis[mini])) {
mini = i;
}
}
if (!mini || dis[mini] == INF)
break;
vis[mini] = 1;
for (auto [v, w] : p[mini]) {
if (vis[v] || w > INF - dis[mini])
continue;
long long nxt = dis[mini] + w;
if (dis[v] > nxt) {
dis[v] = nxt;
}
}
}
return dis;
}Bellman-Ford
Bellman-Ford 基于松弛次数,特殊地,可以用来求解经过边数不超过某个数量的最短路。
原地松弛可以用于普通 Bellman-Ford,并可能在一轮内传播多条边;但若限制至多经过
点击展开代码
cpp
vector<long long> bellman_ford(int s, int n, int k) {
const long long INF = 1LL << 62;
vector<long long> dis(n + 1, INF);
dis[s] = 0;
for (int _ = 1; _ <= k; _++) {
auto pre = dis;
bool flag = 0;
for (int u = 1; u <= n; u++) {
if (pre[u] == INF)
continue;
for (auto [v, w] : p[u]) {
if (dis[v] > pre[u] + w) {
dis[v] = pre[u] + w;
flag = 1;
}
}
}
if (!flag)
break;
}
return dis;
}spfa
SPFA 使用队列只处理本轮距离被更新的顶点,可以看作 Bellman-Ford 的队列优化。普通最短路模板假设从源点不可达负环;若存在可达负环,必须使用后文的检测版本及时终止。
它在部分数据上运行较快,但最坏时间复杂度仍为
标准实现使用 inq 标记顶点是否已在队列中,避免同一个顶点被重复加入队列。
点击展开代码
cpp
vector<long long> spfa(int s, int n) {
const long long INF = 1LL << 62;
vector<long long> dis(n + 1, INF);
vector<bool> inq(n + 1);
dis[s] = 0;
queue<int> q;
q.push(s);
inq[s] = 1;
while (q.size()) {
auto now = q.front();
q.pop();
inq[now] = 0;
for (auto [v, w] : p[now]) {
if (dis[v] > dis[now] + w) {
dis[v] = dis[now] + w;
if (!inq[v]) {
q.push(v);
inq[v] = 1;
}
}
}
}
return dis;
}判负环
从指定源点
Bellman-Ford 版:
点击展开代码
cpp
bool bellman_ford(int s, int n) {
const long long INF = 1LL << 62;
vector<long long> dis(n + 1, INF);
dis[s] = 0;
for (int _ = 1; _ <= n; _++) {
for (int i = 1; i <= n; i++) {
for (auto [v, w] : p[i]) {
if (dis[i] == INF)
continue;
if (dis[v] > dis[i] + w) {
dis[v] = dis[i] + w;
if (_ == n)
return 1;
}
}
}
}
return 0;
}spfa 版:
点击展开代码
cpp
bool spfa(int n) {
vector<long long> dis(n + 1);
vector<int> cnt(n + 1);
vector<bool> inq(n + 1);
queue<int> q;
for (int i = 1; i <= n; i++) {
q.push(i);
inq[i] = 1;
}
while (q.size()) {
auto now = q.front();
q.pop();
inq[now] = 0;
for (auto [v, w] : p[now]) {
if (dis[v] > dis[now] + w) {
dis[v] = dis[now] + w;
cnt[v] = cnt[now] + 1;
if (cnt[v] >= n)
return 1;
if (!inq[v]) {
q.push(v);
inq[v] = 1;
}
}
}
}
return 0;
}Floyd
应先把距离矩阵初始化为 INF,再一次性令 dis[i][i]=0,之后读入边并取最小值。不能在三重循环中反复把对角线改回
点击展开代码
cpp
const long long INF = 1LL << 62;
vector<vector<long long>> dis(n + 1, vector<long long>(n + 1, INF));
for (int i = 1; i <= n; i++)
dis[i][i] = 0;
for (auto [u, v, w] : edge)
dis[u][v] = min(dis[u][v], 1LL * w);
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][k] == INF || dis[k][j] == INF)
continue;
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}算法结束后,若存在
传递闭包
Floyd 可用于求解传递闭包,即全源可达性。特别地,使用 bitset 优化,时间复杂度:
点击展开代码
cpp
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
if (dis[i][k])
dis[i] |= dis[k];
}
}注:Floyd 常数极小,
最小环:
点击展开代码
cpp
auto get_path = [&](auto self, int x, int y) -> void {
if (!pos[x][y])
return;
auto now = pos[x][y];
self(self, x, now);
ans.push_back(now);
self(self, now, y);
};
for (int k = 1; k <= n; k++) {
for (int i = 1; i < k; i++) {
for (int j = i + 1; j < k; j++) {
if (dis[i][j] + Dis[j][k] < minn - Dis[k][i]) {
minn = dis[i][j] + Dis[j][k] + Dis[k][i];
ans.clear();
ans.push_back(k);
ans.push_back(i);
get_path(get_path, i, j);
ans.push_back(j);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][j] > dis[i][k] + dis[k][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
pos[i][j] = k;
}
}
}
}时间复杂度:
Johnson 全源最短路
Johnson 算法用于稀疏图的全源最短路,允许负边,但图中不能存在负环。
新增超级源点
由最短路三角不等式可知
下面用所有 INF 约定的范围内。
点击展开代码
cpp
bool johnson(int n, vector<vector<long long>> &ans) {
const long long INF = 1LL << 62;
vector<long long> h(n + 1);
for (int _ = 1; _ <= n; _++) {
bool flag = 0;
for (auto [u, v, w] : edge) {
if (h[v] > h[u] + w) {
h[v] = h[u] + w;
flag = 1;
}
}
if (!flag)
break;
if (_ == n)
return 0;
}
vector<vector<pair<int, long long>>> g(n + 1);
for (auto [u, v, w] : edge) {
long long nw = (long long)((__int128)w + h[u] - h[v]);
g[u].push_back({v, nw});
}
ans.assign(n + 1, vector<long long>(n + 1, INF));
using pli = pair<long long, int>;
for (int s = 1; s <= n; s++) {
vector<long long> dis(n + 1, INF);
priority_queue<pli, vector<pli>, greater<pli>> q;
dis[s] = 0;
q.push({0, s});
while (q.size()) {
auto [cur, u] = q.top();
q.pop();
if (cur != dis[u])
continue;
for (auto [v, w] : g[u]) {
if (w > INF - dis[u])
continue;
long long nxt = dis[u] + w;
if (dis[v] > nxt) {
dis[v] = nxt;
q.push({nxt, v});
}
}
}
for (int v = 1; v <= n; v++) {
if (dis[v] != INF)
ans[s][v] = (long long)((__int128)dis[v] - h[s] + h[v]);
}
}
return 1;
}使用二叉堆时,时间复杂度为
不同最短路算法的比较
| 最短路算法 | Dijkstra | Bellman-Ford | Floyd | Johnson |
|---|---|---|---|---|
| 图限制 | 非负权图 | 可含负边 | 可含负边 | 可含负边、无负环 |
| 解对象 | 单源最短路 | 单源最短路 | 全源最短路 | 全源最短路 |
| 判负环 | 不能 | 能 | 能 | 能 |
| 时间复杂度 |
0-1 BFS
边权只有 0/1 的最短路问题,使用双端队列维护,若松弛时边权为
使用邻接表时,每个顶点和每条边只会产生常数次有效处理,时间复杂度为
差分约束
差分约束用于解决
根据最短路三角不等式
此外,最短需要一个源点,所以对于上述不等式组,需要做一些额外限制:
- 求解最短路需要源点和所有点连通,所以建图后每一个连通块的求解是独立的。求解一个不等式组,需要钦定一些变量的值,而对应在图中,这些需要被钦定的变量就是强连通分量缩点后入度为
的点。 - 对于原始问题:
的不等式,实际上是有无穷多组解的,因为给每一个元素加一个常数 后,不等式组仍成立。所以求一组解时,可以给每一个元素一个初值的限制也就是超级源点向每一个点连的边权,然后给超级源点钦定一个初值。
最小解
若问题要求每个
使用最长路求解。
注:实际问题中还需要题目对每一个数有最小限制:
最大解:
若问题要求每个
使用最短路求解。
注:实际问题中还需要题目对每一个数有最大限制:
最后
所以实际上,我之前在洛谷板子 P5960 【模板】差分约束 的代码实际上求的是
expand 1.
朴素的,由上可知,差分约束需要每一个不等式严格满足有两个变量
但是对于只有一个变量的不等式,可以转换成和超级源点的连边
expand 2.
如果出现
expand 3.
对于除法不等式形如:
最短路图/最短路树
只考虑从源点
则称它为紧边;由所有紧边组成的子图称为最短路图。从
最短路图不一定是 DAG。紧边环上的等式相加后,环的总边权必为
若最短路图是 DAG,可以拓扑排序后进行路径计数等 dp。若存在
最短路树需要为每个可达的非源点选择一条紧入边。不存在紧边环时可以直接选择;存在
