最短路小结
更新于 2022.9.11
全源最短路径
Johnson 算法
打 ABC 时遇见的新玩意,大意就是对于 SPFA 与 Floyd 不能满足的负权图,先通过势能函数将边权转化为非负数再跑 Dijstra ,难点在于势能函数的建立与求解。
简略证明:
设 \(w(u,v)\) 为边 \((u,v)\) 的边权,引入势能函数后的新边权为 \(w'(u,v)\) ,\(i\) 点的势能为 \(h_i\) ,有如下定义:
\(w'(u,v) = w(u,v) + h_u - h_v \geq 0\)
设存在从 \(s \rightarrow t\) 的一条路径 \(s \rightarrow p_1 \rightarrow p_2 \rightarrow \dots \rightarrow p_k \rightarrow t\) ,则 \(s \rightarrow t\) 的距离为:
\(\space w'(s,p_1) + w'(p_1,p_2) + \dots + w'(p_k,t) \\ = w(s,p_1) + h_s - h_{p_1} + w(p_1,p_2) + h_{p_1} - h_{p_2} + \dots + w(p_k,t) + h_{p_k} - h_t \\ = w(s,p_1) + w(p_1,p_2) + \dots + w(p_k,t) + h_s - h_t\)
不难发现无论 \(s\) 从何路径抵达 \(t\) ,势能差均为 \(h_s - h_t\) ,即只与始末位置有关,正好符合势能的定义。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
using PII = std::pair<int, int>;
using ll = long long;
const int inf = 1e9;
int main()
{
IOS;
int n, m;
std::cin >> n >> m;
std::vector<int> h(n + 1, inf);
std::vector<std::vector<PII>> g(n + 1);
for (int i = 1, u, v, w; i <= m; ++i)
{
std::cin >> u >> v >> w;
g[u].push_back({v, w});
}
for (int i = 1; i <= n; ++i)
g[0].push_back({i, 0});
auto SPFA = [&](int s)
{
std::queue<int> q;
std::vector<int> inq(n + 1), cnt(n + 1);
h[s] = 0;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
inq[u] = false;
for (auto [v, w] : g[u])
{
if (h[v] > h[u] + w)
{
h[v] = h[u] + w;
cnt[v] = cnt[u] + 1;
if (cnt[v] > n)
return true;
if (!inq[v])
q.push(v), inq[v] = true;
}
}
}
return false;
};
if (SPFA(0))
{
std::cout << -1;
return 0;
}
for (int u = 1; u <= n; ++u)
for (auto &[v, w] : g[u])
w += h[u] - h[v];
auto Dijkstra = [&](int s)
{
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> q;
std::vector<int> dis(n + 1, inf), vis(n + 1);
dis[s] = 0;
q.push({0, s});
while (!q.empty())
{
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({dis[v], v});
}
}
}
return dis;
};
for (int i = 1; i <= n; ++i)
{
auto dis = Dijkstra(i);
ll ans = 0;
for (int j = 1; j <= n; ++j)
{
if (dis[j] != inf)
dis[j] -= (h[i] - h[j]);
ans += 1ll * j * dis[j];
}
std::cout << ans << '\n';
}
return 0;
}
#include<bits/stdc++.h>
#define IOS std::ios::sync_with_stdio(false);\
std::cin.tie(0);\
std::cout.tie(0);
using PII = std::pair<int,int>;
using ll = long long;
const ll inf = 1e18;
int main() {
IOS;
int n, m;
std::cin >> n >> m;
std::vector<int> h(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> h[i];
auto calc = [&](int u, int v){
return h[u] >= h[v] ? 0 : h[v] - h[u];
};
std::vector<std::vector<PII>> g(n + 1);
for (int i = 1, u, v; i <= m; ++i)
{
std::cin >> u >> v;
g[u].push_back({v, calc(u, v)});
g[v].push_back({u, calc(v, u)});
}
auto Dijkstra = [&](int s)
{
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> q;
std::vector<ll> dis(n + 1, inf);
std::vector<bool> vis(n + 1);
dis[s] = 0;
q.push({0, s});
while (!q.empty())
{
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({dis[v], v});
}
}
}
return dis;
};
std::vector<ll> dis = Dijkstra(1);
ll ans = 0;
for (int i = 1; i <= n; ++i)
if (dis[i] != inf)
ans = std::max(ans, -(dis[i] - (h[1] - h[i])));
std::cout << ans;
return 0;
}
单源最短路径
DIjkstra 算法
auto Dijkstra = [&](int s)
{
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> q;
std::vector<int> dis(n + 1, inf), vis(n + 1);
dis[s] = 0;
q.push({0, s});
while (!q.empty())
{
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({dis[v], v});
}
}
}
return dis;
};
SPFA 算法
auto SPFA = [&](int s)
{
std::queue<int> q;
std::vector<int> dis(n + 1, inf), inq(n + 1);
dis[s] = 0;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
inq[u] = false;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
if (!inq[v])
q.push(v), inq[v] = true;
}
}
}
return dis;
};
任意两点间距离
Floyd 算法
auto Floyd = [&](std::vector<std::vector<int>> &f)
{
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
f[i][j] = std::min(f[i][j], f[i][k] + f[k][j]);
};
关于 Dijkstra 算法的一些思考
思考1:在所有边权值非负的前提下,Dijkstra 算法一定正确吗?
先在原图上以 \(1\) 为起点,求出从 \(1\) 走到 \(x\) 的所有路径中,能够经过的权值最小的节点权值 \(d[x]\)
再建反图,以 \(n\) 为起点,求出从 \(n\) 走到 \(x\) 的所有路径中,能够经过的权值最大的节点权值 \(f[x]\)
最后枚举每个节点 \(x\) ,求出 \(max(f[x]-d[x])\) 的最大值即可。
现在问题在于算法的选择,由于此题不是求和而是求最值,\(Dijkstra\) 算法并不适用,因为 \(Dijkstra\) 每轮一定会确定到一个点的最短距离,并且这个点之后一定不可能再被更新。因此对于最值而言,当前最值并不一定是最终最值。
例如存在边 \((x,y),(y,z),(z,x)\) ,假设最开始更新到 \(d[x] = a\) ,节点 \(y,z\) 的点权分别为 \(b,c\) 且存在 \(a > b > c\) ,那么 \(d[x]\) 的值显然应该被更新成 \(c\) ,但如果采用 \(Dijkstra\) 算法,之前由于已经更新过 \(d[x]\) ,我们认为其最值“已被确定”,因此更新并不会发生,所以本题中 \(Dijkstra\) 算法并不适用,而是应该采用 \(SPFA\) 算法。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define pii std::pair<int, int>
const int inf = 0x3f3f3f3f;
int main()
{
IOS;
int n, m;
std::cin >> n >> m;
std::vector<int> p(n + 1);
std::vector<std::vector<int>> g1(n + 1), g2(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> p[i];
for (int i = 1, x, y, z; i <= m; ++i)
{
std::cin >> x >> y >> z;
g1[x].push_back(y);
g2[y].push_back(x);
if (z == 2)
{
g1[y].push_back(x);
g2[x].push_back(y);
}
}
auto spfa = [&](std::vector<std::vector<int>> &g, int st, std::vector<int> &dis, int flag)
{
std::vector<bool> in(n + 1);
std::queue<int> q;
dis[st] = p[st], in[st] = true;
q.push(st);
while (!q.empty())
{
int u = q.front();
q.pop();
in[u] = false;
for (auto v : g[u])
{
int w = flag ? std::max(dis[u], p[v]) : std::min(dis[u], p[v]);
if (flag ? (dis[v] < w) : (dis[v] > w))
{
dis[v] = w;
if (!in[v])
q.push(v), in[v] = true;
}
}
}
};
std::vector<int> d(n + 1, inf), f(n + 1);
spfa(g1, 1, d, 0);
spfa(g2, n, f, 1);
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = std::max(ans, f[i] - d[i]);
std::cout << ans << std::endl;
return 0;
}
思考2:在存在负边权值的情况下,Dijkstra 算法一定不适用吗?
首先用 \(tarjan\) 对连通图进行缩点,再把单向边添加进图中得到一张有向无环图。之后按拓扑序依次处理各连通块,在块内跑 \(Dijkstra\) 算法求出该块内的最短路信息即可。
但我们发现,在用当前连通块节点更新其它连通块节点的最短路时,是涉及到了负边权的,那为什么此时 \(Dijkstra\) 算法仍然适用呢?思考1中我们提到: \(Dijkstra\) 每轮一定会确定到一个点的最短距离,并且这个点之后一定不可能再被更新,但因为负权边可能使得某个已经确定最短距离的点距离变得更短,于是就会得到错误答案。
而对于这题,由于负边权所在的边是单向边且缩点后的图保证不是一张强连通图,所以通过负边权更新的距离,一定不会被用于更新之前已经确定的距离。通俗地说,就是经过这些负边后,我们一定不会回头去更新已确定最短路长度的点。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define PII std::pair<int, int>
int main()
{
IOS;
int n, r, p, s;
std::cin >> n >> r >> p >> s;
std::vector<int> bel(n + 1);
std::vector<std::vector<PII>> g(n + 1);
for (int i = 1, x, y, z; i <= r; ++i)
{
std::cin >> x >> y >> z;
g[x].push_back({y, z});
g[y].push_back({x, z});
}
int cnt = 0;
std::function<void(int, int)> dfs = [&](int u, int now)
{
bel[u] = now;
for (auto [v, w] : g[u])
if (!bel[v])
dfs(v, now);
};
for (int i = 1; i <= n; ++i)
if (!bel[i])
dfs(i, ++cnt);
std::vector<int> c[cnt + 1];
for (int i = 1; i <= n; ++i)
c[bel[i]].push_back(i);
std::vector<int> dis(n + 1, 0x3f3f3f3f), deg(cnt + 1);
dis[s] = 0;
for (int i = 1, x, y, z; i <= p; ++i)
{
std::cin >> x >> y >> z;
g[x].push_back({y, z});
++deg[bel[y]];
}
std::vector<bool> vis(n + 1);
std::queue<int> q;
for (int i = 1; i <= cnt; ++i)
if (!deg[i])
q.push(i);
auto dijkstra = [&](int st)
{
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> pq;
for (auto x : c[st])
pq.push({dis[x], x});
while (!pq.empty())
{
int u = pq.top().second;
pq.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
if (bel[u] == bel[v])
pq.push({dis[v], v});
}
if (bel[u] != bel[v] && !--deg[bel[v]])
q.push(bel[v]);
}
}
};
while (!q.empty())
{
int u = q.front();
q.pop();
dijkstra(u);
}
for (int i = 1; i <= n; ++i)
{
if (dis[i] >= 1e9)
std::cout << "NO PATH" << std::endl;
else
std::cout << dis[i] << std::endl;
}
return 0;
}
思考3:为什么很多情况下 Dijkstra 跑不了的 SPFA 却能跑? SPFA 的各种优化真的有效吗?
由于 \(SPFA\) 不会在某个时间点确定某个点的最短距离,只有当队列为空,即满足所有三角不等式 \(dis[y] \leq dis[x] + z\) 时,才算确定所有点的最短距离。
众所周知, OI 界流传着“SPFA已死”这种说法,这种说法不无它的道理,由于在菊花图上 \(SPFA\) 的复杂度会退化为 \(O(nm)\) ,因此大多数时候仍应使用 \(Dijkstra\) 算法求最短路。
然而,这并不能打消大家使用 \(SPFA\) 的念头,有关 \(SPFA\) 的优化方法也是层出不穷,本篇并不过多赘述这些优化方法。因为随着 OI 界普遍水平的提高、命题规范的完善, \(SPFA\) 的用武之地只会越来越少。针对 2022 年 ICPC 澳门场的构造,这里大致贴一下知乎上看到的有关一些比较知名的 \(SPFA\) 优化的卡法:
-
\(LLL\) 优化:每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾。
-
\(Hack\) :向 1 连接一条权值巨大的边,这样 \(LLL\) 就失效了。
-
\(SLF\) 优化:每次将入队结点距离和队首比较,如果更大则插入至队尾。
-
\(Hack\) :使用链套菊花的方法,在链上用几个并列在一起的小边权边就能欺骗算法多次进入菊花。
-
\(mcfx\) 优化:在第 \([L,R]\) 次访问一个结点时,将其放入队首,否则放入队尾。通常取 \(L = 2,R = \sqrt(V)\) 。
-
\(Hack\):网格图表现优秀,但是菊花图表现很差。
-
\(SLF + swap\) :每当队列改变时,如果队首距离大于队尾,则交换首尾。
-
\(Hack\): 与卡 \(SLF\) 类似,外挂诱导节点即可。
从原理上分析,所有 \(SPFA\) 的优化都是为了使队列接近优先队列。然而,我们知道维护一个优先队列在目前来说是需要 \(log\) 的复杂度的,所以低于该复杂度的一定能 \(Hack\) 。
———— 转自 如何看待 SPFA 算法已死这种说法? fstqwq 的回答。
应用汇总
分层图
一般模型:在一个正常的图上可以进行 \(k\) 次决策,对于每次决策,不影响图的结构,只影响目前的状态或代价。一般将决策前的状态和决策后的状态之间连接一条权值为决策代价的边,表示付出该代价后就可以转换状态了。
一般有两种方法解决分层图最短路问题:
-
建图时直接建成 \(k+1\) 层跑最短路。
-
仿照动态规划思想,多开一维记录机会信息。
解法一:
建 \(k+1\) 层图,在从一个点到另一个点时,如果选择走边权为 0 的边,就进入下一层,相当于进行一次免费操作。共有 \((k+1) \times n\) 个点, \(1 \sim n\) 表示第一层, \((1+n) \sim (n+n)\) 代表第二层, \((1+2 \times n) \sim (n+2 \times n)\) 代表第三层, \((1+i \times n)\sim(n+i \times n)\) 代表第 \(i+1\) 层。答案就是 \(n , 2 \times n, 3 \times n \dots i \times n\) 中的最小值。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define PII std::pair<int, int>
const int inf = 0x3f3f3f3f;
int main()
{
IOS;
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::vector<PII>> g((k + 1) * n + 1);
for (int i = 1, x, y, z; i <= m; ++i)
{
std::cin >> x >> y >> z;
for (int j = 1; j <= k + 1; ++j)
{
g[(j - 1) * n + x].push_back({(j - 1) * n + y, z});
g[(j - 1) * n + y].push_back({(j - 1) * n + x, z});
if (j != k + 1)
{
g[(j - 1) * n + x].push_back({j * n + y, 0});
g[(j - 1) * n + y].push_back({j * n + x, 0});
}
}
}
auto dijkstra = [&](int st)
{
std::vector<int> vis, dis;
vis = dis = std::vector<int>((k + 1) * n + 1);
std::fill(dis.begin(), dis.end(), inf);
dis[st] = 0;
std::priority_queue<PII, std::vector<PII>, std::greater<PII>> q;
q.push({0, st});
while (!q.empty())
{
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
w = std::max(dis[u], w);
if (dis[v] > w)
{
dis[v] = w;
q.push({dis[v], v});
}
}
}
int res = inf;
for (int i = 1; i <= k + 1; ++i)
res = std::min(res, dis[i * n]);
if (res == inf)
res = -1;
return res;
};
std::cout << dijkstra(1) << std::endl;
return 0;
}
解法二:
-
dis[i][j]
代表到达i
用了j
次免费机会的最小花费。 -
vis[i][j]
代表到达i
用了j
次免费机会的情况是否出现过。
如果存在一条从 \(x\) 到 \(y\) 长度为 \(z\) 的无向边,有下列转移方程:
-
\(dis[y][p] = max(dis[x][p], z)\)
-
\(dis[y][p] = dis[x][p-1]\)
前者表示不在边 \((x,y,z)\) 上使用免费机会,后者表示使用。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define PII std::pair<int, int>
const int inf = 0x3f3f3f3f;
struct node
{
int x, y, z;
friend bool operator<(node lft, node rht) { return lft.y > rht.y; };
};
int main()
{
IOS;
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::vector<PII>> g(n + 1);
for (int i = 1, x, y, z; i <= m; ++i)
{
std::cin >> x >> y >> z;
g[x].push_back({y, z});
g[y].push_back({x, z});
}
auto dijkstra = [&](int st)
{
std::vector<std::vector<bool>> vis(n + 1, std::vector<bool>(k + 1));
std::vector<std::vector<int>> dis(n + 1, std::vector<int>(k + 1, inf));
for (int i = 1; i <= k; ++i)
dis[0][i] = 0;
dis[st][0] = 0;
std::priority_queue<node> q;
q.push({st, 0, 0});
while (!q.empty())
{
node now = q.top();
q.pop();
int u = now.x, p = now.z;
if (vis[u][p])
continue;
vis[u][p] = true;
for (auto [v, w] : g[u])
{
w = std::max(dis[u][p], w);
if (dis[v][p] > w)
{
dis[v][p] = w;
q.push({v, dis[v][p], p});
}
if (p < k && dis[v][p + 1] > dis[u][p])
{
dis[v][p + 1] = dis[u][p];
q.push({v, dis[v][p + 1], p + 1});
}
}
}
int ans = inf;
for (int i = 0; i <= k; ++i)
ans = std::min(ans, dis[n][i]);
return ans == inf ? -1 : ans;
};
std::cout << dijkstra(1) << std::endl;
return 0;
}
最短路计数
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
const int P = 100003;
int main()
{
IOS;
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> g(n + 1);
for (int i = 1, u, v; i <= m; ++i)
{
std::cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
auto bfs = [&](int s)
{
std::vector<int> dis(n + 1), cnt(n + 1), vis(n + 1);
cnt[s] = vis[s] = 1;
std::queue<int> q;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
for (auto v : g[u])
{
if (!vis[v])
{
vis[v] = 1;
dis[v] = dis[u] + 1;
q.push(v);
}
if (dis[v] == dis[u] + 1)
{
cnt[v] = (cnt[v] + cnt[u]) % P;
}
}
}
return cnt;
};
std::vector<int> ans = bfs(1);
for (int i = 1; i <= n; ++i)
std::cout << ans[i] << std::endl;
return 0;
}
次短路计数
通过两次 \(bfs\) 分别计算出从 \(s\) 和 \(t\) 出发到各顶点的最短路长度与数量。然后我们可以枚举所有的边 \((u,v)\) 。如果下列两个条件均成立:
-
顶点 \(u\) 和 \(v\) 应该包括在同一层次(与 \(s\) 的距离相同)
-
\(dis(s,u)+dis(v,t)=dis(s,t)\)
那么我们就可以建立从 \(s\) 到 \(t\) ,长度等于 \(dis(s,t) + 1\) 的次短路 \(s→...→u→v→...→t\) ,再利用乘法原理即可求出对答案的总贡献。
上述第一个条件是用于去重的,在此给出粗略证明:我们考虑从 \(s\) 走到 \(t\) ,那么每经过一条边,要么与 \(s\) 的距离变化 1 ,要么不变。考虑是次短路,所以一定存在经过某一条边时,与 \(s\) 的距离不变。这样的边具有唯一性,所以我们只需枚举并找出所有满足这些条件的边,判断其路径长度是否满足条件即可。
#include <bits/stdc++.h>
#define PII std::pair<int, int>
#define w first
#define c second
using ll = long long;
const int P = 1e9 + 7;
char buf[1 << 23], *p1 = buf, *p2 = buf, obuf[1 << 23], *O = obuf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch))
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch))
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
ll solve()
{
int n = read(), m = read(), s = read(), t = read();
std::vector<std::vector<int>> g(n + 1);
std::vector<PII> e;
for (int i = 1, u, v; i <= m; ++i)
{
u = read(), v = read();
g[u].push_back(v), g[v].push_back(u);
e.push_back({u, v});
}
auto bfs = [&](int st)
{
std::vector<PII> dis(n + 1);
std::vector<bool> vis(n + 1);
std::queue<int> q;
q.push(st);
dis[st] = {0, 1}, vis[st] = true;
while (!q.empty())
{
int u = q.front();
q.pop();
for (auto v : g[u])
{
if (!vis[v])
{
dis[v].w = dis[u].w + 1;
vis[v] = true;
q.push(v);
}
if (dis[v].w == dis[u].w + 1)
{
dis[v].c = (dis[v].c + dis[u].c) % P;
}
}
}
return dis;
};
std::vector<PII> ds = bfs(s), dt = bfs(t);
ll ans = ds[t].c;
for (auto [u, v] : e)
{
if (ds[u].w ^ ds[v].w)
continue;
if (ds[u].w + dt[v].w == ds[t].w)
ans = (ans + 1ll * ds[u].c * dt[v].c % P) % P;
if (ds[v].w + dt[u].w == ds[t].w)
ans = (ans + 1ll * ds[v].c * dt[u].c % P) % P;
}
return ans;
}
int main(int argc, char const *argv[])
{
int t = read();
while (t--)
printf("%lld\n", solve());
return 0;
}
判负环
考虑 Bellman-Ford 算法的原理,对于最短路存在的图,松弛操作最多只会执行 \(n - 1\) 轮,因此如果第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(s\) 点出发,能够抵达一个负环。代码实现采用了常数较小的 SPFA 算法:
auto SPFA = [&](int s)
{
std::queue<int> q;
std::vector<int> dis(n + 1, inf), inq(n + 1), cnt(n + 1);
dis[s] = 0;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
inq[u] = false;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n)
return true;
if (!inq[v])
q.push(v), inq[v] = true;
}
}
}
return false;
};
但如果图并不连通,要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点跑 SPFA 即可。注意判负环时由于增加了 0 号点,因此只有当循环轮数 \(> n\) 时才说明有负环 。
多源多汇最短路
模型一: \(n\) 个点, \(m\) 条双向边,现有 \(p\) 个特殊点,求从任意一个点出发,到附近最近特殊点的最短距离。
解法:一般这种题型的 \(n,m\) 均大于 \(Floyd\) 的承受范围,因此不妨转换思路,建立一个虚拟节点 \(s\),然后 \(s\) 向每个特殊点(或是起点)都连一条距离为 \(0\) 的边,再以 \(s\) 为起点跑 \(Dijkstra\) 求出 \(s\) 到各点的最短路,即可得任意一点到附近最近特殊点的最短路。
模型二:给你一张有向图,存在多个起点,多个终点,可以从任一起点出发,求到达任一终点的最短距离。
解法:同理,建立一个超级源点 \(s\),然后 \(s\) 向每个起点都连一条距离为 \(0\) 的边,建立一个超级汇点 \(t\),然后每个终点都向 \(t\) 连一条距离为 \(0\) 的边,之后跑 \(Dijkstra\) 即可。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define PII std::pair<int, int>
using ll = long long;
const ll inf = 1e18;
struct node
{
ll dist;
int id, city;
friend bool operator<(const node lft, const node rht) { return lft.dist > rht.dist; };
};
int main()
{
IOS;
int n, m, k, l;
std::cin >> n >> m >> k >> l;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> a[i];
std::vector<int> p;
for (int i = 1, x; i <= l; ++i)
{
std::cin >> x;
p.push_back(x);
}
std::vector<std::vector<PII>> g(n + 1);
for (int i = 1, x, y, z; i <= m; ++i)
{
std::cin >> x >> y >> z;
g[x].push_back({y, z});
g[y].push_back({x, z});
}
std::vector<ll> ans(n + 1, inf);
auto dijkstra = [&]()
{
std::vector<int> vis(n + 1), upd(n + 1);
std::priority_queue<node> q;
for (auto x : p)
q.push({0, x, a[x]});
while (!q.empty())
{
node now = q.top();
q.pop();
int u = now.id;
if (vis[u] > 1 || upd[u] == now.city)
continue;
if (a[u] != now.city)
ans[u] = std::min(ans[u], now.dist);
++vis[u], upd[u] = now.city;
for (auto [v, w] : g[u])
q.push({now.dist + w, v, now.city});
}
return ans;
};
dijkstra();
for (int i = 1; i <= n; ++i)
std::cout << (ans[i] == inf ? -1 : ans[i]) << " ";
return 0;
}
传递闭包
离散数学的知识,如果对每一 \(x,y,z \in A\) , \(xRy\) , \(yRz\) 蕴含着 \(xRz\) ,那么 \(R\) 是传递的。关系的闭包运算是关系上的一元运算,它把给出的关系 \(R\) 扩充成一新关系 \(R'\) ,使 \(R'\) 具有一定的性质,且所进行的扩充又是最“节约”的。至于这个传递闭包则可以通过 \(Floyd\) 求出。
#include <bits/stdc++.h>
int main(int argc, char const *argv[])
{
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> f(n + 1, std::vector<int>(n + 1));
for (int i = 1; i <= n; ++i)
f[i][i] = 1;
for (int i = 1, u, v; i <= m; ++i)
{
std::cin >> u >> v;
f[u][v] = f[v][u] = 1;
}
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
f[i][j] |= f[i][k] & f[k][j];
return 0;
}
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define id(x) (x - 'A' + 1)
int main()
{
IOS;
int n, m;
std::string s;
auto flody = [&](std::vector<std::vector<int>> &g)
{
std::vector<std::vector<int>> f(n + 1, std::vector<int>(n + 1));
f = g;
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
{
f[i][j] |= f[i][k] & f[k][j];
if (i != j && f[i][j] && f[j][i])
return -1;
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (i != j && !f[i][j] && !f[j][i])
return 0;
return 1;
};
while (std::cin >> n >> m && n && m)
{
bool flag = true;
std::vector<std::vector<int>> g(n + 1, std::vector<int>(n + 1));
for (int i = 1; i <= m; ++i)
{
std::cin >> s;
int u = id(s[0]), v = id(s[2]);
g[u][v] = 1;
if (flag)
{
int now = flody(g);
if (u == v)
now = -1;
if (now == -1)
{
flag = false;
std::cout << "Inconsistency found after " << i << " relations." << std::endl;
}
else if (now == 1)
{
flag = false;
std::cout << "Sosed sequence determined after " << i << " relations: ";
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] |= g[i][k] & g[k][j];
std::vector<int> ans(n + 1);
for (int i = 1; i <= n; ++i)
{
int cnt = 0;
for (int j = 1; j <= n; ++j)
if (i != j && g[j][i])
++cnt;
ans[cnt + 1] = i;
}
for (int i = 1; i <= n; ++i)
std::cout << (char)(ans[i] + 'A' - 1);
std::cout << "." << std::endl;
}
}
}
if (flag)
std::cout << "Sosed sequence cannot be determined." << std::endl;
}
return 0;
}
最小环
考虑 \(Floyd\) 算法的过程:当外层循环 \(k\) 刚开始时, \(f[i,j]\) 保存着“经过编号不超过 \(k-1\) 的节点”从 \(i\) 到 \(j\) 的最短路长度,即从 \(i\) 到 \(j\) 的最短路必然不经过 \(k\) 。
因此在第 \(k\) 次循环的内层循环时,如果存在边 \(e(i,k)\) 和边 \(e(k,j)\) , \(k \rightarrow i \rightarrow j \rightarrow k\) 即形成了一个可能的最小环,至少包含三个点,且权值为 \(dis(i,j) + w(i,k) + w(k,j)\) 。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
using ll = long long;
const ll inf = 0x3f3f3f3f;
int main()
{
IOS;
int n, m;
ll ans = inf;
std::cin >> n >> m;
std::vector<std::vector<ll>> f, g;
f = g = std::vector<std::vector<ll>>(n + 1, std::vector<ll>(n + 1, inf));
for (int i = 1; i <= n; ++i)
g[i][i] = 0;
for (int i = 1, x, y, z; i <= m; ++i)
{
std::cin >> x >> y >> z;
g[x][y] = g[y][x] = std::min(g[x][y], 1ll * z);
}
f = g;
std::vector<int> path; //具体方案
std::vector<std::vector<int>> pos(n + 1, std::vector<int>(n + 1));
std::function<void(int, int)> getPath = [&](int x, int y)
{
if (!pos[x][y])
return;
getPath(x, pos[x][y]);
path.push_back(pos[x][y]);
getPath(pos[x][y], y);
};
for (int k = 1; k <= n; ++k)
{
for (int i = 1; i < k; ++i)
for (int j = i + 1; j < k; ++j)
if (ans > f[i][j] + g[i][k] + g[k][j])
{
ans = f[i][j] + g[i][k] + g[k][j];
path.clear();
path.push_back(k);
path.push_back(i);
getPath(i, j);
path.push_back(j);
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (f[i][j] > f[i][k] + f[k][j])
f[i][j] = f[i][k] + f[k][j], pos[i][j] = k;
}
if (ans == inf)
std::cout << "No solution.";
else
for (auto x : path)
std::cout << x << " ";
std::cout << std::endl;
return 0;
}
题意:对于给定带权无向图的每个顶点,计算包含该顶点的最小环长度。
思路:以每个点为源点 \(s\) 跑 \(Dijkstra\) 算法,得到一棵以其为根的最短路树。用 HLD 为 LCA 查询预处理该树。
考虑不在最短路树上的边 \(e = \{ u, v \}\) 。如果 \(LCA(u, v) = s\) ,那么该路径是一个包含当前根节点的简单环,其权重为 \(dis[u] + dis[v] + w(e)\) 。顶点 \(s\) 的答案即是所有这类环的最小值。
#include <bits/stdc++.h>
#define IOS \
std::ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0);
#define PLL std::pair<ll, ll>
using ll = long long;
const ll inf = 1e18;
struct HLD
{
int N;
std::vector<std::vector<int>> &G;
std::vector<int> fa, siz, dep, son, top;
HLD(std::vector<std::vector<int>> &G, int root = 0) : N(G.size()), G(G), fa(N, 0), siz(N, 0), dep(N, 0), son(N, 0), top(N, 0)
{
dfs1(root, 0);
dfs2(root, root);
};
void dfs1(int u, int f)
{
siz[u] = 1, fa[u] = f, dep[u] = dep[f] + 1;
for (auto v : G[u])
if (v != f)
{
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, int t)
{
top[u] = t;
if (son[u])
dfs2(son[u], t);
for (auto v : G[u])
if (v != fa[u] && v != son[u])
dfs2(v, v);
}
int lca(int x, int y)
{
while (top[x] ^ top[y])
{
if (dep[top[x]] < dep[top[y]])
std::swap(x, y);
x = fa[top[x]];
}
if (dep[x] > dep[y])
std::swap(x, y);
return x;
}
};
int main()
{
IOS;
int n;
std::cin >> n;
std::vector<std::vector<PLL>> g(n + 1);
for (int i = 1, w; i <= n; ++i)
{
for (int j = 1; j <= n; ++j)
{
std::cin >> w;
if (w > 0)
g[i].push_back({j, w});
}
}
std::vector<ll> ans(n + 1, -1ll);
for (int s = 1; s <= n; ++s)
{
std::vector<bool> vis(n + 1);
std::vector<int> fa(n + 1);
std::vector<ll> dis(n + 1, inf);
std::priority_queue<PLL, std::vector<PLL>, std::greater<PLL>> pq;
dis[s] = 0;
pq.push({0, s});
while (!pq.empty())
{
int u = pq.top().second;
pq.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto [v, w] : g[u])
{
if (dis[v] > dis[u] + w)
{
fa[v] = u;
dis[v] = dis[u] + w;
pq.push({dis[v], v});
}
}
}
std::vector<std::vector<int>> G(n + 1);
for (int i = 1; i <= n; ++i)
if (fa[i])
G[fa[i]].push_back(i);
HLD hld(G, s);
for (auto [v, w] : g[s])
{
if (fa[v] == s)
continue;
if (ans[s] == -1ll || ans[s] > dis[v] + w)
ans[s] = dis[v] + w;
}
for (int u = 1; u <= n; ++u)
{
if (u == s)
continue;
if (dis[u] == inf)
continue;
for (auto [v, w] : g[u])
{
if (v == s)
continue;
if (hld.lca(u, v) != s)
continue;
if (ans[s] == -1ll || ans[s] > dis[u] + dis[v] + w)
ans[s] = dis[u] + dis[v] + w;
}
}
}
for (int i = 1; i <= n; ++i)
std::cout << ans[i] << std::endl;
return 0;
}