学习笔记-最短路
觉得讲的不详细可以去B站看:https://www.bilibili.com/video/av85550343
1. 正权无向图最小环问题(floyd)
引用链接:点我
抛开Dijkstra算法,进而我们想到用Floyd算法。我们知道,Floyd算法在进行时会不断更新矩阵dist(k)。设dist[k,i,j]表示从结点i到结点j且满足所有中间结点,它们均属于集合{1,2,⋯ ,k}的一条最短路径的权。其中dist[0,i,j ]即为初始状态i到j的直接距离。对于一个给定的赋权有向图, 求出其中权值和最小的一个环。我们可以将任意一个环化成如下形式:u->k->v ->(x1-> x2-> ⋯ xm1)-> u(u与k、k与v都是直接相连的),其中v ->(x1-> 2-> ⋯ m)-> u是指v到u不经过k的一种路径。
在u,k,v确定的情况下,要使环权值最小, 则要求 (x1一>x2->⋯一>xm)->u路径权值最小.即要求其为v到u不经过k的最短路径,则这个经过u,k,v的环的最短路径就是:[v到u不包含k的最短距离]+dist[O,u,k]+dist[O,k,v]。我们用Floyd只能求出任意2点间满足中间结点均属于集合{1,2,⋯ ,k}的最短路径,可是我们如何求出v到u不包含k的最短距离呢?
现在我们给k加一个限制条件:k为当前环中的序号最大的节点(简称最大点)。因为k是最大点,所以当前环中没有任何一个点≥k,即所有点都<k。因为v->(x1->x2->......xm)->u属于当前环,所以x1,x2,⋯ ,xm<k,即x1,x2.⋯。xm≤k一1。这样,v到u的最短距离就可以表示成dist[k一1 ,u,v]。dist[k一1,v,u]表示的是从v到u且满足所有中间结点均属于集合{1,2,⋯ ,k一1}的一条最短路径的权。接下来,我们就可以求出v到u不包含k的最短距离了。这里只是要求不包含k,而上述方法用的是dist[k一1,v,u],求出的路径永远不会包含k+l,k+2,⋯ 。万一所求的最小环中包含k+1,k+2,⋯ 怎么办呢?的确,如果最小环中包含比k大的节点,在当前u,k,v所求出的环显然不是那个最小环。然而我们知道,这个最小环中必定有一个最大点kO,也就是说,虽然当前k没有求出我们所需要的最小环,但是当我们从k做到kO的时候,这个环上的所有点都小于kO了.也就是说在k=kO时一定能求出这个最小环。我们用一个实例来说明:假设最小环为1—3—4—5—6—2—1。的确,在u=l,v=4,k=3时,k<6,dist[3,4,1]的确求出的不是4—5—6—2—1这个环,但是,当u=4,v=6,k=5或u=5,v=2,k=6时,dist[k,v,u]表示的都是这条最短路径.所以我们在Floyd以后,只要枚举u.v,k三个变量即可求出最小环。时间复杂度为O(n3)。我们可以发现,Floyd和最后枚举u,v,k三个变量求最小环的过程都是u,v,k三个变量,所以我们可以将其合并。这样,我们在k变量变化的同时,也就是进行Floyd算法的同时,寻找最大点为k的最小环。
讲的听清楚的。
下面是我的板子(例题HDU1599):
#include <bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0)
#define forn(i, n) for(int i = 0; i < n; ++i)
const int inf = 2e7;
int dis[105][105], a[105][105];
int main() {
IO;
int n, m;
while(cin >> n >> m) {
forn(i, 105) forn(j, 105) dis[i][j] = a[i][j] = inf;
forn(i, m) {
int u, v, w;
cin >> u >> v >> w;
dis[u][v] = min(w, dis[u][v]);
a[u][v] = a[v][u] = dis[v][u] = dis[u][v];
}
int ans = inf;
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] + a[i][k] + a[k][j]);
}
}
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
if(ans != inf) cout << ans << '\n';
else cout << "It's impossible." << '\n';
}
return 0;
}
2. Johnson算法
引用链接:点我
Johnson 和 Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。该算法在 1977 年由 Donald B. Johnson 提出。
Part 1 算法概述
任意两点间的最短路可以通过枚举起点,跑 nn 次 Bellman-Ford 算法解决,时间复杂度是 O(n^2m)O(n2m) 的,也可以直接用 Floyd 算法解决,时间复杂度为 O(n^3)O(n3) 。
注意到堆优化的 Dijkstra 算法求单源最短路径的时间复杂度比 Bellman-Ford 更优,如果枚举起点,跑 nn 次 Dijkstra 算法,就可以在 O(nm\log m)O(nmlogm) (本文中的 Dijkstra 采用 priority_queue
实现,下同)的时间复杂度内解决本问题,比上述跑 nn 次 Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。
但 Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。
一种容易想到的方法是给所有边的边权同时加上一个正数 xx ,从而让所有边的边权均非负。如果新图上起点到终点的最短路经过了 kk 条边,则将最短路减去 kxkx 即可得到实际最短路。
但这样的方法是错误的。考虑下图:
1 \to 21→2 的最短路为 1 \to 5 \to 3 \to 21→5→3→2,长度为 -2−2。
但假如我们把每条边的边权加上 55 呢?
新图上 1 \to 21→2 的最短路为 1 \to 4 \to 21→4→2 ,已经不是实际的最短路了。
Johnson 算法则通过另外一种方法来给每条边重新标注边权。
我们新建一个虚拟节点(在这里我们就设它的编号为 00 )。从这个点向其他所有点连一条边权为 00 的边。
接下来用 Bellman-Ford 算法求出从 00 号点到其他所有点的最短路,记为 h_ihi 。
假如存在一条从 uu 点到 vv 点,边权为 ww 的边,则我们将该边的边权重新设置为 w+h_u-h_vw+hu−hv 。
接下来以每个点为起点,跑 nn 轮 Dijkstra 算法即可求出任意两点间的最短路了。
容易看出,该算法的时间复杂度是 O(nm\log m)O(nmlogm) 。
Q:那这么说,Dijkstra 也可以求出负权图(无负环)的单源最短路径了?
A:没错。但是预处理要跑一遍 Bellman-Ford,还不如直接用 Bellman-Ford 呢。
Part 2 正确性证明
为什么这样重新标注边权的方式是正确的呢?
在讨论这个问题之前,我们先讨论一个物理概念——势能。
诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。
势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。
接下来回到正题。
在重新标记后的图上,从 ss 点到 tt 点的一条路径 s \to p_1 \to p_2 \to \dots \to p_k \to ts→p1→p2→⋯→pk→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,p1)+hs−hp1)+(w(p1,p2)+hp1−hp2)+⋯+(w(pk,t)+hpk−ht)
化简后得到:
w(s,p_1)+w(p_1,p_2)+ \dots +w(p_k,t)+h_s-h_tw(s,p1)+w(p1,p2)+⋯+w(pk,t)+hs−ht
无论我们从 ss 到 tt 走的是哪一条路径, h_s-h_ths−ht 的值是不变的,这正与势能的性质相吻合!
为了方便,下面我们就把 h_ihi 称为 ii 点的势能。
上面的新图中 s \to ts→t 的最短路的长度表达式由两部分组成,前面的边权和为原图中 s \to ts→t 的最短路,后面则是两点间的势能差。因为两点间势能的差为定值,因此原图上 s \to ts→t 的最短路与新图上 s \to ts→t 的最短路相对应。
到这里我们的正确性证明已经解决了一半——我们证明了重新标注边权后图上的最短路径仍然是原来的最短路径。接下来我们需要证明新图中所有边的边权非负,因为在非负权图上,Dijkstra 算法能够保证得出正确的结果。
根据三角形不等式,新图上任意一边 (u,v)(u,v) 上两点满足: h_v \leq h_u + w(u,v)hv≤hu+w(u,v) 。这条边重新标记后的边权为 w'(u,v)=w(u,v)+h_u-h_v \geq 0w′(u,v)=w(u,v)+hu−hv≥0 。这样我们证明了新图上的边权均非负。
至此,我们就证明了 Johnson 算法的正确性。
我的代码:(洛谷P5905)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define forn(i, n) for(int i = 0; i < n; ++i)
#define for1(i, n) for(int i = 1; i <= n; ++i)
#define IO ios::sync_with_stdio(false);cin.tie(0)
const int maxn = 3e3 + 5;
const int inf = 1e9;
int n, m;
map<int,int> mp[maxn];
vector<pair<int,int> >e[maxn], g[maxn];
int dis[maxn], h[maxn], vis[maxn];
bool inq[maxn];
void spfa() {
queue<int>q;
for1(i, n) {
h[i] = 0;
inq[i] = 1;
q.push(i);
}
while(!q.empty()) {
int u = q.front(); q.pop();
inq[u] = 0;
for(auto x : e[u]) {
int w = x.second, v = x.first;
if(h[v] > h[u] + w) {
h[v] = h[u] + w;
if(!inq[v]) {
q.push(v), inq[v] = 1;
++vis[v];
if(vis[v] == n) {
cout << -1 << '\n';
exit(0);
}
}
}
}
}
for1(u, n) {
for(auto &x : e[u]) {
int v = x.first, w = x.second;
g[u].push_back({v, w + h[u] - h[v]});
}
}
}
void dij(int s) {
for1(i, n) dis[i] = inf, vis[i] = 0;
priority_queue<pair<int,int> >pq;
pq.push({0, s});
dis[s] = 0;
while(!pq.empty()) {
auto now = pq.top(); pq.pop();
int u = now.second;
if(vis[u]) continue;
vis[u] = 1;
for(auto &x : g[u]) {
int v = x.first, w = x.second;
if(vis[v]) continue;
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.push({-dis[v],v});
}
}
}
ll ans = 0;
for1(i, n) {
if(dis[i] == inf) ans += 1ll * i * inf;
else ans += 1ll * i * (dis[i] - h[s] + h[i]);
}
// if(s == 1)cerr<<'\n';
cout << ans << '\n';
}
int main() {
IO;
forn(i, maxn) h[i] = inf;
cin >> n >> m;
forn(i, m) {
int u, v, w; cin >> u >> v >> w;
if(u == v) {
if(w < 0) return cout << -1 << '\n', 0;
continue;
}
if(!mp[u].count(v)) mp[u][v] = w;
else mp[u][v] = min(mp[u][v], w);
}
for1(i, n) {
for(auto &x : mp[i]) {
e[i].push_back({x.first, x.second});
}
}
spfa();
for1(i, n) dij(i);
return 0;
}
3. 有三道还挺有意思的例题,我放在B站讲了:https://space.bilibili.com/255125226,有两道题搜不到一道题是bzoj4289
4. 最短路计数:两道洛谷例题P1608、P1144还有一道NOIP2017(P3953)提高组最后一题,也是最短路计数。
P1144题意:n个点m条边无向无权图,求1-n的最短路方案数。数据范围很小,但是可以O(n)做。
我们知道最短路取一个点的dis时条件是dis[v] > dis[u] + w(u-v)。而有时候会有dis[v] == dis[u] + w(u-v),也就是这种条件会对种类数++,然后按照DP的思路往下走就OK,因为BFS和dij都是经过的点不会再次访问所以就没有任何问题。
代码:
#include <bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0)
#define forn(i, n) for(int i = 0; i < n; ++i)
#define for1(i, n) for(int i = 1; i <= n; ++i)
const int maxn = 1e6 + 5;
const int mod = 100003;
vector<int>e[maxn];
bool vis[maxn];
int dis[maxn], ans[maxn];
int main() {
IO;
int n, m; cin >> n >> m;
forn(i, m) {
int u, v; cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
queue<int>que;
que.push(1);
dis[1] = 0, ans[1] = 1, vis[1] = 1;
while(!que.empty()) {
int u = que.front(); que.pop();
for(auto v : e[u]) {
if(!vis[v]) dis[v] = dis[u] + 1, ans[v] = ans[u], vis[v] = 1, que.push(v);
else if(dis[v] == dis[u] + 1) (ans[v] += ans[u]) %= mod;
}
}
for1(i, n) cout << ans[i] << '\n';
return 0;
}
P1608只是把图改成有向带权而已(也就是dij)
#include <bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0)
#define ll long long
#define forn(i, n) for(int i = 0; i < n; ++i)
#define for1(i, n) for(int i = 1; i <= n; ++i)
const int inf = 2e9;
const int maxn = 2e3 + 5;
int g[maxn][maxn];
vector<pair<int, int> >e[maxn];
bool vis[maxn];
int dis[maxn];
ll ans[maxn];
int main() {
IO;
//freopen("P1608_5.in", "r", stdin);
int n, m; cin >> n >> m;
forn(i, m) {
int u, v, w; cin >> u >> v >> w;
if(!g[u][v]) g[u][v] = w;
else g[u][v] = min(g[u][v], w);
}
for1(u, n) {
for1(v, n) {
if(u == v) continue;
if(!g[u][v]) continue;
//cerr << "@#! " << u << ' '<< v << ' '<< g[u][v] << '\n';
e[u].push_back({v, g[u][v]});
}
}
priority_queue<pair<int, int> >pq;
pq.push({0, 1});
for1(i, n) dis[i] = inf;
dis[1] = 0, ans[1] = 1;
while(!pq.empty()) {
auto now = pq.top(); pq.pop();
int u = now.second;
if(vis[u]) continue;
vis[u] = 1;
for(auto &x : e[u]) {
int v = x.first, w = x.second;
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
ans[v] = ans[u];
pq.push({-dis[v], v});
}else if(dis[v] == dis[u] + w) ans[v] += ans[u];
}
}
if(dis[n] == inf) cout << "No answer" << '\n';
else cout << dis[n] << ' ' << ans[n] << '\n';
return 0;
}
P3953就很有意思了,设1-n最短路长为X 题意要找的是1-n路径长度在[x, x + d]之间。
洛谷很多博客说用topu序+dp搞,随便弄个样例就hack了,topu序的前提条件是DAG,只是洛谷的数据弱。但80%的博客都在讲
用topu序就很离谱。剩下有10%说跑spfa,更离谱。spfa极限复杂度O(n*m).
那么真正的做法是什么?
这道题的d最大才50,那么我们开个Dp[maxn][50],dpij表示的是在点i比最短路多j的方案数,然后倒着跑一遍DP,在起点取ans就可以了。
#include <bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0)
#define ll long long
#define forn(i, n) for(int i = 0; i < n; ++i)
#define for1(i, n) for(int i = 1; i <= n; ++i)
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const int inf = 2e9;
bool ok = 1;
int n, m, k, mod, tot;
struct edage {
int nex, v, w;
}e[maxm], g[maxm];
int head[maxn], head2[maxn], dis[maxn];
bool vis[maxn], viss[maxn][55], visss[maxn][55];
ll dp[maxn][55];
inline int dfs(int u, int val) {
if(visss[u][val]) return dp[u][val];
viss[u][val] = visss[u][val] = 1;
for(int i = head2[u]; i; i = g[i].nex) {
int v = g[i].v, w = g[i].w;
int nval = val + dis[u] - dis[v] - w;
//cerr << "!@# "<< u << ' ' << v << ' ' << w << ' ' << nval << '\n';
if(nval < 0) continue;
if(viss[v][nval]) {
ok = 0;
viss[u][val] = 0;
return 0;
}
(dp[u][val] += dfs(v, nval)) %= mod;
}
viss[u][val] = 0;
return dp[u][val];
}
inline void dij() {
forn(i, n + 5) dis[i] = inf, vis[i] = 0;
priority_queue<pair<int, int> >pq;
pq.push({0, 1});
dis[1] = 0;
while(!pq.empty()) {
auto now = pq.top(); pq.pop();
int u = now.second;
//cerr << u << ' ' << vis[u] << '\n';
if(vis[u]) continue;
vis[u] = 1;
for(int i = head[u]; i; i = e[i].nex) {
int v = e[i].v, w = e[i].w;
//cerr << u << ' ' << v << ' '<< w << ' ' << dis[v] << ' '<< '\n';
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.push({-dis[v], v});
}
}
}
}
inline void add(int u, int v, int w) {
e[++tot] = {head[u], v, w}, head[u] = tot;
g[tot] = {head2[v], u, w}, head2[v] = tot;
}
inline void init() {
tot = 0, ok = 1;
forn(i, n + 5) {
forn(j, k + 5) dp[i][j] = visss[i][j] = 0;
}
forn(i, m + 5) head[i] = head2[i] = 0;
}
int main() {
IO;
//freopen("park.in", "r", stdin);
int T; cin >> T; while(T--) {
cin >> n >> m >> k >> mod;
//cerr << n << ' ' << m << ' ' << k << ' ' << mod << '\n';
init();
forn(i, m) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dij();
ll ans = 0;
add(n + 1, 1, 0);
dp[n + 1][0] = 1, visss[n + 1][0] = 1, dis[n + 1] = 0;
forn(i, k + 1) (ans += dfs(n, i)) %= mod;
if(ok) cout << ans << '\n';
else cout << -1 << '\n';
}
return 0;
}