概念
- 单源最短路:给出一点 \(S\) ,求 \(S\) 到所有点的最短路
- 多源最短路:给出多点 \(S\) ,求 \(S\) 到所有点的最短路
- 全源最短路:给出所有点 \(S\) ,求 \(S\) 到所有点的最短路
实现
Floyd 算法
Floyd 算法基于 DP 思想,采用邻接矩阵存边
复杂度比较高,但是常数小,容易实现
适用于所有图,不管有向无向,边权正负,但是最短路必须存在。(但是不能有负环)
具体思路:枚举所有的两两点的组合以及每一个组合的“中转点”,再进行松弛操作
设 \(a_{x,y}\) 表示 \(x \to y\) 的最短路径长度
初始化:
在求单源最短路径的时候就会浪费许多空间,但在求多源最短路时,复杂度仍是 $ O(n^{3}) $
代码示例:
inline void Floyd() {
for (int k = 1; k <= n; ++k) // 枚举中转点
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
}
在用 Floyd 转递闭包时,我们可以考虑优化一点点:
在枚举两两的中转点时,我们枚举了很多不连通的点,而这些点是没有贡献的
所以我们可以用 bitset 存边,每次用 _Find_next 寻找下一个联通的点
时间复杂度为 \(O(\dfrac{n^{3}}{k})\)(k是一个常数)
应用
首先其一定是一个简单环
在枚举 \(k_{i}\) 的时候,我们已经得到了前 \(k-1\) 个点的最短路径
f[x][y] 和 \((k,x), (k,y)\)共同构成了环。
所以连接 $ x \to y \to k \to x $ ,我们就得到了一个经过 \(x , y , k\) 的最小环
在 Floyd 的过程中枚举 \(k\) ,计算这个和的最小值即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e2 + 7;
ll dis[N][N], mp[N][N];
ll ans = inf;
int n, m;
inline void init() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i != j)
dis[i][j] = mp[i][j] = inf;
}
inline void Floyd() {
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = i + 1; j < k; ++j)
ans = min(ans, dis[i][j] + mp[i][k] + mp[k][j]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[j][i] = dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
signed main() {
scanf("%d%d", &n, &m);
init();
for (int i = 1, u, v; i <= m; ++i) {
ll w;
scanf("%d%d%lld", &u, &v, &w);
dis[u][v] = dis[v][u] = min(dis[u][v], w);
mp[u][v] = mp[v][u] = min(mp[u][v], w);
}
Floyd();
if (ans == inf)
printf("No solution.");
else
printf("%lld", ans);
return 0;
}
SPFA 算法
SPFA基于广搜思想,是经过队列优化的 Bellman-Ford 算法
我们用 dis 数组记录源点到图上任意一点距离,其中源点到自身距离为 \(0\) ,到其他点距离为 \(\infty\)
将源点入队,并重复以下步骤:
- 队首 \(x\) 出队
- 遍历所有以队首为起点的有向边 \(x \to i\) ,若 \(dis_x + w(x,i) < dis_i\) ,则更新 \(dis_i\)
- 如果点i不在队列中,则 \(i\) 入队
- 若队列为空,跳出循环;否则执行1
但是这么写在有源点可以到达的负环的情况下会T飞
考虑最短路存在的时候。
由于一次松弛操作会使最短路的边数至少 \(+1\) ,而最短路的边数最多为 \(n-1\)。
所以最多执行 次松弛操作,\(n-1\) 即最多循环 \(n-1\) 次。
那么一个点入队次数 \(\geq n\) ,就会存在负环,我们就可以退出
如果图是随机的,时间复杂度为 \(O(KE)\)
但是实际上 SPFA 的最坏复杂度是 \(O(VE)\) ,可以构造出卡 SPFA 的数据,让 SPFA 超时。
代码:
inline bool SPFA(int s) {
memset(dis, inf, sizeof(dis));
memset(cnt, 0, sizeof(cnt));
memset(inque, false, sizeof(inque));
queue<int> q;
q.push(s);
inque[s] = true, dis[s] = 0, cnt[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = false;
for (int i = 0, v, w; i < e[u].size(); ++i) {
v = e[u][i].first, w = e[u][i].second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!inque[v]) {
q.push(v), inque[v] = true, ++cnt[v];
if (cnt[v] >= n)
return false; // 有负环
}
}
}
}
return true;
}
优化
- LLL 优化:每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾
- SLF 优化:每次将入队结点距离和队首比较,如果更大则插入至队尾
- SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾
- mcfx 优化:定义区间 \([l, r]\) ,当入队点入队次数属于这个区间时从队首插入,反之从队尾插入
- SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾
- 这个 SLF 看起来很弱,但却通过了所有 Hack 数据,而且非常难卡
卡 SPFA
首先要清楚卡 SPFA 的原理是让点尽可能多次的入队,反复更新
如果一个点被多个边连着,那么当这些边先依次走向它的时候,这个点就会被调用很多次,然后往下走,于是可以构造一个链套菊花,这样对于菊花的那个点会反复入队很多次,然后往下每次都要走很多次,于是就死了。
另一种卡法是构造一个网格图,行比列小得多,比如 \(10 \times 10^4\) 之类的,然后对于竖着的边权设得很小,横着的边权设的很大,如 \(1\) 和 \(rand() \bmod 10^4 + 10\) 。因为如果在网格图中走错了一次,就会走很多步无用步,于是就死了。
但是每次将进队多次得点放到队首可以优化复杂度
考虑构造这样的边:\((1, i, 2(n - i + 1) + 1), (i, i - 1, 1)\) ,即可
那么结合两种卡法,对于普通的SPFA最好的卡法就是将图构造为一个网格套链套菊花
针对优化的 Hack
- LLL 优化:向 \(1\) 连一条权值巨大的边
- SLF 优化:链套菊花
- SLF带容错:卡 SLF 的做法,并开打大边权总和,最好超过 \(10^{12}\)
- mcfx 优化:菊花图
- SLF + swap:与卡 SLF 类似,外挂诱导节点即可
Dijkstra 算法
首先,在这个算法中,有两种点,一种是已经确定最短路径的点(又称为蓝点),一种是尚未确定最短路径的点(又称为白点)。蓝点的最短路径不会再改变
这样为什么就能得出从起点出发所有点的最短路径呢?
首先,所有点都是蓝点,起点到起点的最短路径为 \(0\)
然后要找出一个当前没有到达过的点(白点)中路径最短的点,记为 \(u\) 。将 \(u\) 设为蓝点(确定它的最短路径)。
那为什么当前最短路径最短的白点,可以转化为蓝点呢?
反证法:
如果 \(u\) 的 \(dis\) 值还能更新,考虑从 \(S\) 到它的最短路径,其倒数第二个点,一定是现在的白点(如果它的 \(dis\) 值还能更新,又不是由蓝点更新的,自然就是由白点更新的)。设这个白点为 v,既然 \(u\) 是当前 \(dis\) 最小的,\(dis_{v} > dis_{u}\),那 \(dis_v + w{v -> u}\) 不可能 \(> dis_{u}\)
万一有负边权呢?
给每条边加上一个数字的做法显然是错误的,如果这样处理,最短路径会极大地受到经过边数的影响。
事实上 Dijkstra 算法只能解决正权图上的最短路问题问题
复杂度分析:外层一个循环,去除最小值一个循环,所以时间复杂度就是 $ O(n^{2}) $
事实上,对于取出当前白点里 \(dis\) 值最小的这一步,我们可以用堆优化 ,时间复杂度降至 $ O(n \log n + m) $
代码:
inline void Dijkstra(int s) {
memset(dis, inf, sizeof(dis));
memset(vis, false, sizeof(vis));
priority_queue<pair<int, int> > q;
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 (int i = 0, v, w; i < e[u].size(); ++i) {
v = e[u][i].first, w = e[u][i].second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push(make_pair(-dis[v], v));
}
}
}
}
另一种写法(取消了 \(vis\) 数组):
inline void Dijkstra(int s) {
memset(dis, inf, sizeof(dis));
priority_queue<pair<int, int> > q;
dis[s] = 0, q.push({0, s});
while (!q.empty()) {
pair<int, int> c = q.top();
q.pop();
if (dis[c.second] != -c.first)
continue;
int u = c.second;
for (int i = 0, v, w; i < e[u].size(); ++i) {
v = e[u][i].first, w = e[u][i].second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push(make_pair(-dis[v], v));
}
}
}
}
Johnson 全源最短路
为了解决 Dijkstra 跑不了负边权的问题,Johnson提出了一种船新的办法。
我们建一个超级源点,所有点与其连一条边权为 \(0\) 的边。
先采用 SPFA 算法将每个点与 超级源点的最短路径长度算出来,这里记作 \(h_i\) 。
设一条有向边 \(u \to v\) 边权为 \(w\) ,那么将每条边的边权增加 \(h_u-h_v\) ,当然,最后统计答案的时候要将其减去,接下来就可以直接跑 Dijkstra了
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 3e3 + 7;
vector<pair<int, int>> e[N];
int dis[N][N];
int h[N], cnt[N];
bool inque[N], vis[N];
int n, m;
inline void AddEdge(int u, int v, int w) {
e[u].push_back(make_pair(v, w));
}
inline bool SPFA(int s) {
memset(h, inf, sizeof(h));
queue<int> q;
q.push(s);
h[s] = 0, inque[s] = true, cnt[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inque[u] = false;
for (int i = 0, v, w; i < e[u].size(); ++i) {
v = e[u][i].first, w = e[u][i].second;
if (h[v] > h[u] + w) {
h[v] = h[u] + w;
if (!inque[v]) {
q.push(v);
inque[v] = true;
++cnt[v];
if (cnt[v] > n)
return false;
}
}
}
}
return true;
}
inline void Dijkstra(int *d, int s) {
memset(vis, false, sizeof(inque));
priority_queue<pair<int, int> > q;
d[s] = 0;
q.push({0, s});
while (!q.empty()) {
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (int i = 0, v, w; i < e[u].size(); ++i) {
v = e[u][i].first, w = e[u][i].second + h[u] - h[v];
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
q.push(make_pair(-d[v], v));
}
}
}
}
inline bool Johnson() {
for (int i = 1; i <= n; ++i)
AddEdge(0, i, 0);
if (!SPFA(0))
return false;
memset(dis, inf, sizeof(dis));
for (int i = 1; i <= n; ++i)
Dijkstra(dis[i], i);
return true;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v, w; i <= m; ++i) {
scanf("%d%d%d", &u, &v, &w);
AddEdge(u, v, w);
}
if (!Johnson())
return puts("-1"), 0;
for (int i = 1; i <= n; ++i) {
ll ans = 0;
for (int j = 1; j <= n; ++j) {
if (dis[i][j] == inf)
ans += 1ll * j * 1e9;
else
ans += 1ll * j * (dis[i][j] - h[i] + h[j]);
}
printf("%lld\n", ans);
}
return 0;
}
扩展
最短路图
即求出所有最短路(多条也算)组成的图,往往在求最值时第一关键字为最短路时配合 DP 使用
首先求出最短路图,那么只要在图上找到平方和最大的路径
这里的最短路图是 DAG, 于是可以按拓扑序设计 DP
设 \(dp_x\) 表示以 \(x\) 为终点的最大权值,枚举上一个换乘点,有
斜率优化即可,复杂度瓶颈为最短路