本节介绍单源最短路与其他算法的结合。
一、新年好
最短路与DFS的结合。题目要求从 1 1 1 号点开始,以任意顺序访问 a , b , c , d , e a,b,c,d,e a,b,c,d,e 五个点的最短路程。(如按照 a b c d e abcde abcde 的顺序即 1 → a → b → . . . → e 1\to a\to b\to ...\to e 1→a→b→...→e)
若确定了一个访问顺序后,要使总路程最短,每一段走的都要是最短路。一个想法是,先DFS出访问顺序,对于每一个访问顺序 x 1 x 2 x 3 x 4 x 5 x_1x_2x_3x_4x_5 x1x2x3x4x5,用六次最短路,分别求出 1 1 1 到 x 1 x_1 x1, x 1 x_1 x1 到 x 2 x_2 x2,…, x 4 x_4 x4 到 x 5 x_5 x5 的最短路程,相加即为总路程,最后取最小值。若用时间复杂度为 O ( k M ) O(kM) O(kM) 的SPFA,计算次数约为 5 ! × 5 × 1 0 5 k = 6 × 1 0 7 k 5!×5×10^5k=6×10^7k 5!×5×105k=6×107k 有超时风险。
不难发现上述方案有非常多的冗余部分:每个访问顺序都要求一次最短路。可以将求最短路与DFS出访问顺序二者对调,即先预处理出六次最短路 (分别以 1 a b c d e 1abcde 1abcde 为起点) 的结果,再在DFS访问顺序的同时计算出总路程。这里用堆优化Dijkstra实现。
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair <int, int> PII;
const int N = 50010, M = 2e5 + 10, INF = 0x3f3f3f3f;
int n, m;
int source[6];
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[6][N];
bool st[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void dijkstra(int start, int dist[]){
memset(dist, 0x3f, N * 4);
memset(st, 0, sizeof st);
dist[start] = 0;
priority_queue <PII, vector <PII>, greater <PII>> heap;
heap.push({0, start});
while (!heap.empty()){
int t = heap.top().second;
heap.pop();
if (st[t]) continue;
st[t] = 1;
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
heap.push({dist[j], j});
}
}
}
}
int dfs(int u, int start, int distance){
if (u == 6) return distance;
int res = INF;
for (int i = 1; i <= 5; i ++)
if (!st[i]){
int next = source[i];
st[i] = 1;
res = min(res, dfs(u + 1, i, distance + dist[start][next]));
st[i] = 0;
}
return res;
}
int main(){
scanf("%d %d", &n, &m);
source[0] = 1;
for (int i = 1; i <= 5; i ++) scanf("%d", &source[i]);
memset(h, -1, sizeof h);
while (m --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < 6; i ++) dijkstra(source[i], dist[i]);
memset(st, 0, sizeof st);
printf("%d\n", dfs(1, 0, 0));
return 0;
}
二、通信线路
最短路与二分的结合。(最大值最小)
定义在
[
0
,
1
0
6
+
1
]
[0,10^6+1]
[0,106+1] 的区间中的性质:
对于区间中的某个数
x
x
x,求出从
1
1
1 走到
N
N
N,最少经过的长度大于
x
x
x 的边的数量是否小于等于
K
K
K。
设
a
n
s
ans
ans 是最终答案,可以证明:所有大于等于
a
n
s
ans
ans 的数都满足上述性质,所有小于
a
n
s
ans
ans 的数都不满足。
证:
a
n
s
ans
ans 是最终答案,即在某一条从
1
1
1 走到
N
N
N 的路径中,去掉前
K
K
K 条权值最高的边后,
a
n
s
ans
ans 是剩下边权中最小的。在这条路径上,长度大于
a
n
s
ans
ans 的边等于
K
K
K,因此
a
n
s
ans
ans 满足性质。
显然,对于所有大于
a
n
s
ans
ans 的数
x
x
x,
x
x
x 越大,边权大于
x
x
x 的边数只会减少,一定满足性质。
若存在小于
a
n
s
ans
ans 的数
x
x
x 满足性质,那么
a
n
s
ans
ans 就不是题意所求的最小值了,与
a
n
s
ans
ans 是最终答案矛盾,因此不满足性质。
有了这一性质,本题就可用二分求解。还剩下两个问题:
- 如何求出从
1
1
1 到
N
N
N 最少经过长度大于
x
x
x 的边的数量?
可以将所有边分类,若边长大于 x x x,则边权看成 1 1 1,否则边权看成 0 0 0,用双端队列BFS求改变权值后 1 1 1 到 N N N 的最短路。 - 定义区间为何包含了
0
0
0 和
1
0
6
+
1
10^6+1
106+1?
当 K K K 大于等于某一从 1 1 1 到 N N N 的路径上边的数量时,不用付钱, 0 0 0 可以是答案。
当题目无解时,从 1 1 1 到 N N N 的最短路为无穷大,大于 K K K,二分的最后结果会是区间右端点。若右端点为 1 0 6 10^6 106,无法区分是无解还是有解且答案恰为 1 0 6 10^6 106,因此右端点取大于 1 0 6 10^6 106 的数 1 0 6 + 1 10^6+1 106+1 以区分无解。
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <deque>
using namespace std;
const int N = 1010, M = 20010;
int n, m, k;
int h[N], e[M], w[M], ne[M], idx;
deque <int> q;
int dist[N];
bool st[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool check(int bound){
memset(st, 0, sizeof st);
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
q.push_back(1);
while (!q.empty()){
int t = q.front();
q.pop_front();
if (st[t]) continue;
st[t] = 1;
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i], v = w[i] > bound;
if (dist[j] > dist[t] + v){
dist[j] = dist[t] + v;
if (!v) q.push_front(j);
else q.push_back(j);
}
}
}
return dist[n] <= k;
}
int main(){
cin >> n >> m >> k;
memset(h, -1, sizeof h);
while (m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e6 + 1;
while (l < r){
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
if (r == 1e6 + 1) r = -1;
cout << r << endl;
return 0;
}
三、道路与航线
单源最短路与拓扑排序的结合。
已知本题SPFA会被卡。考虑题目给定的两种路径的特点:
- 道路,双向,边权非负
- 航线,单向,边权任意,无环,且如果有一条航线从 A A A 到 B B B,则 B B B 无法通过道路和航线回到 A A A
航线单向且无环,想到拓扑图,但是除航线外还有双向的非负权边。由于航线具有的特殊性质,若将所有航线全部去掉后,所有点会形成多个“团状”:团内部的点由双向边连接,团与团之间互不相通 (仅通过航线相连,且航线单向),如下图。
在每个团内部,用Dijkstra算法求出两点间的最短路,时间复杂度 O ( M log N ) O(M\log N) O(MlogN);在团与团之间,按线性时间复杂度的拓扑序扫描。
具体实现步骤:
- 先输入所有双向道路,然后DFS出所有连通块,计算两个数组:
id[]
存储每个点属于哪个连通块;vector <int> block[]
存储每个连通块里有哪些点; - 输入所有航线,同时统计每个连通块的入度;
- 按照拓扑序依次处理每个连通块,先将所有入度为零的连通块的编号加入队列中;
- 每次从队头取出一个连通块的编号
bid
; - 将
block[id]
中的所有点加入堆中,然后对堆中所有点跑Dijkstra算法; - 每次取出堆中距离最小的点
ver
,遍历其所有邻点j
; - 如果
id[ver] == id[j]
,那么如果j
能被更新,则将j
插入堆中;如果id[ver] != id[j]
,则将id[j]
这个连通块的入度减一,如果入度为零,则将其插入拓扑排序的队列中。
总时间复杂度 O ( M log N ) O(M\log N) O(MlogN)
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <vector>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 25010, M = 150010, INF = 0x3f3f3f3f;
int n, mr, mp, S;
int h[N], e[M], w[M], ne[M], idx;
int id[N];
vector <int> block[N];
int bcnt;
int dist[N], din[N];
bool st[N];
queue <int> q;
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u, int bid){
id[u] = bid;
block[bid].push_back(u);
for (int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if (!id[j]) dfs(j, bid);
}
}
void dijkstra(int bid){
priority_queue <PII, vector <PII>, greater <PII>> heap;
for (auto ver : block[bid]) heap.push({dist[ver], ver});
while (!heap.empty()){
auto t = heap.top();
heap.pop();
int ver = t.y;
if (st[ver]) continue;
st[ver] = 1;
for (int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[ver] + w[i]){
dist[j] = dist[ver] + w[i];
if (id[j] == id[ver]) heap.push({dist[j], j});
}
if (id[j] != id[ver]){
din[id[j]] --;
if (!din[id[j]]) q.push(id[j]);
}
}
}
}
void topsort(){
memset(dist, 0x3f, sizeof dist);
dist[S] = 0;
for (int i = 1; i <= bcnt; i ++)
if (!din[i])
q.push(i);
while (!q.empty()){
int t = q.front();
q.pop();
dijkstra(t);
}
}
int main(){
scanf("%d %d %d %d", &n, &mr, &mp, &S);
memset(h, -1, sizeof h);
while (mr --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 1; i <= n; i ++)
if (!id[i])
dfs(i, ++ bcnt);
while (mp --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
din[id[b]] ++;
}
topsort();
for (int i = 1; i <= n; i ++)
if (dist[i] > INF / 2) puts("NO PATH");
else printf("%d\n", dist[i]);
return 0;
}
四、最优贸易
最短路与DP的交集很大,可以将DP中的某一状态看成图论中的一个点,状态间的转移看成边。如01背包问题,状态转移方程为 f [ i , j ] = m a x { f [ i − 1 , j ] , f [ i − 1 , j − v i ] + w i } f[i,j]=max\{f[i-1,j],f[i-1,j-v_i]+w_i\} f[i,j]=max{f[i−1,j],f[i−1,j−vi]+wi},可以看成这样的点和边的关系:
进而,01背包问题可以转化为这样的图论问题:终点为
f
[
n
,
m
]
f[n,m]
f[n,m],起点为
f
[
0
,
0
]
,
f
[
0
,
1
]
,
.
.
.
,
f
[
0
,
m
]
f[0,0],f[0,1],...,f[0,m]
f[0,0],f[0,1],...,f[0,m],求出起点到终点的最长路径。
诸如此类,且绝大多数DP问题的依赖关系之间没有环,因此它们都可以转化为拓扑图上的最短(长)路问题,用线性扫描的方式求解。
当DP的依赖关系不具有拓扑序时,可以用最短路的方式求最优解。譬如有这样的关系: f [ 2 ] = m i n { f [ 1 ] + 1 , f [ 4 ] + 1 } , f [ 3 ] = f [ 2 ] + 1 , f [ 4 ] = f [ 3 ] + 1 , f [ 5 ] = f [ 4 ] + 1 f[2]=min\{f[1]+1,f[4]+1\},f[3]=f[2]+1,f[4]=f[3]+1,f[5]=f[4]+1 f[2]=min{f[1]+1,f[4]+1},f[3]=f[2]+1,f[4]=f[3]+1,f[5]=f[4]+1,求 f [ 5 ] f[5] f[5]。可以化成最短路问题:
图中边权均为
1
1
1,求
1
1
1 到
5
5
5 的最短路,容易得出结果为
4
4
4,即
f
[
5
]
=
f
[
1
]
+
4
f[5]=f[1]+4
f[5]=f[1]+4。
(另,若都是等式关系,可以用高斯消元求解。)
回到本题,本题是最短路与DP的结合。从
1
1
1 号点到
N
N
N 号点先买后卖的所有方案,可以按照买和卖的分界点划分成
N
N
N 个子集 (子集间可能有交集),只要分别求出这
N
N
N 个子集中的方案的最大值,最后取max即可。
考虑求买和卖分界点为
i
i
i 号点 (即在
1
1
1 到
i
i
i 过程中买,在
i
i
i 到
N
N
N 过程中卖) 的方案的最大值:先求出从
1
1
1 到
i
i
i 过程中买入价格的最小值
d
m
i
n
[
i
]
dmin[i]
dmin[i],再求出从
i
i
i 到
N
N
N 中卖出价格的最大值
d
m
a
x
[
i
]
dmax[i]
dmax[i],两者相减。
设
s
1
,
s
2
,
.
.
.
,
s
t
s_1,s_2,...,s_t
s1,s2,...,st 是所有经过一条边就能到达
i
i
i 的点,可得状态转移方程:
d
m
i
n
[
i
]
=
m
i
n
{
d
m
i
n
[
s
1
]
,
d
m
i
n
[
s
2
]
,
.
.
.
,
d
m
i
n
[
s
t
]
,
w
i
}
dmin[i]=min\{dmin[s_1],dmin[s_2],...,dmin[s_t],w_i\}
dmin[i]=min{dmin[s1],dmin[s2],...,dmin[st],wi}。
这个状态转移关系可能有环,因此不能用递推的方式直接求出, 转化成最短路问题。
(单源)最短路有Dijkstra和SPFA两种方法,但是这里只能选用SPFA。从两种算法的原理出发:
- Dijkstra的核心在于,每次从堆中取出取到最小值的点后,它的最短路一定不会再被更新。但是本题不满足这个性质,考虑如下例子:
当 2 2 2 号点第一次出队时, d m i n [ 2 ] = 5 dmin[2]=5 dmin[2]=5,但此后的 3 3 3 号点会更新 d m i n [ 2 ] = 4 dmin[2]=4 dmin[2]=4。
- SPFA (或Bellman-Ford) 本质是DP,迭代 N − 1 N-1 N−1 次,每次用三角不等式更新所有点。SPFA是基于边数的,当经过 K K K 次迭代时,从起点走经过不超过 K K K 条边的最短路被确定。只要在没有负环的情况下,SPFA都是正确的。
注意,在求 d m a x dmax dmax 时,需要在反向图上求,其余求解过程与 d m i n dmin dmin 类似。
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 2e6 + 10;
int n, m;
int w[N];
int hs[N], ht[N], e[M], ne[M], idx;
int dmin[N], dmax[N];
int q[N];
bool st[N];
void add(int h[], int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void spfa(int h[], int dist[], int type){
int hh = 0, tt = 1;
if (!type){
memset(dist, 0x3f, sizeof dmin);
dist[1] = w[1];
q[0] = 1;
}
else{
memset(dist, -0x3f, sizeof dmax);
dist[n] = w[n];
q[0] = n;
}
while (hh != tt){
int t = q[hh ++];
if (hh == N) hh = 0;
st[t] = 0;
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (!type && dist[j] > min(dist[t], w[j]) || type && dist[j] < max(dist[t], w[j])){
if (!type) dist[j] = min(dist[t], w[j]);
else dist[j] = max(dist[t], w[j]);
if (!st[j]){
q[tt ++] = j;
if (tt == N) tt = 0;
st[j] = 1;
}
}
}
}
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);
memset(hs, -1, sizeof hs);
memset(ht, -1, sizeof ht);
while (m --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(hs, a, b), add(ht, b, a);
if (c == 2) add(hs, b, a), add(ht, a, b);
}
spfa(hs, dmin, 0);
spfa(ht, dmax, 1);
int res = 0;
for (int i = 1; i <= n; i ++)
res = max(res, dmax[i] - dmin[i]);
printf("%d\n", res);
return 0;
}