本节介绍单源最短路的扩展:虚拟源点、分层图、最短路计数、单源次短路。
一、选择最佳线路
本题中有多个起点,求出从这些起点中的某一个到达终点的最短距离。由于起点只有一个,一种想法是在反向图上求终点到各个起点的最短路,取最小值,但是若终点也有多个时便无法处理,这种方法难以扩展。另一种想法是分别从每个起点跑一遍最短路,取最小值,时间复杂度为 O ( k S M ) O(kSM) O(kSM),且题目有多组测试数据,当起点数量 S S S 很大时会超时。
我们可以引入一个虚拟源点,将其作为单源最短路的起点,其向每个起点都连有一条边权为 0 0 0 的边。当有多个终点时,也可将全部终点连接在一个类似的虚拟源点上。这样引入虚拟源点的思想其实在2.1.2多源BFS中已有体现,只不过当时比较抽象,是直接将所有起点全部入队。当然,本题也可直接将所有起点放入堆中,效果与引入虚拟源点相同。
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010, M = 21010, INF = 0x3f3f3f3f;
int n, m, T;
int h[N], e[M], w[M], ne[M], idx;
int dist[N], q[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 ++;
}
int spfa(){
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;
int hh = 0, tt = 1;
q[0] = 0;
st[0] = 1;
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 (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if (!st[j]){
q[tt ++] = j;
if (tt == N) tt = 0;
st[j] = 1;
}
}
}
}
if (dist[T] == INF) return -1;
return dist[T];
}
int main(){
while (scanf("%d %d %d", &n, &m, &T) != -1){
memset(h, -1, sizeof h);
idx = 0;
while (m --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
int s;
scanf("%d", &s);
while (s --){
int ver;
scanf("%d", &ver);
add(0, ver, 0);
}
printf("%d\n", spfa());
}
return 0;
}
二、拯救大兵瑞恩
如何处理钥匙和锁着的门?如果仍然用原本的 dist[]
数组存储到达每一个点的最短距离,发现这样是很难转移的——无法判断当前手中有没有钥匙,能不能穿过某扇门。一个直观的想法是类似状态压缩DP,增加状态的维数,用二进制数同时存储当前手中的钥匙。
用闫氏DP分析法:
- 状态表示:
d
[
x
,
y
,
s
t
a
t
e
]
d[x,y,state]
d[x,y,state]
(1) 集合:所有从起点走到 ( x , y ) (x,y) (x,y),且当前拥有的钥匙是 s t a t e state state 的所有路线的集合
(2) 属性:最小值 - 状态计算:一个状态可以更新哪些状态
(1) 当前位置 ( x , y ) (x,y) (x,y) 处有一把钥匙 k e y key key,直接拿起,钥匙变成 s t a t e o r k e y state\ or\ key state or key,无需任何花费
(2) 向上下左右四个方向前进一步,只有当无门无墙,或者有门且有匹配的钥匙时才可前进,花费为 1 1 1
传统的DP求解方式是递推,前提是状态之间的依赖关系具有拓扑序,即在计算某个状态时,所有会更新它的状态均已被计算过。但是本题的状态之间会存在环形的依赖关系,无法用递推。类似3.1.2单源最短路的综合应用中的最优贸易一题,这样具有环形依赖关系的DP问题可以用最短路的方式求解。
将每个状态 d [ x , y , s t a t e ] d[x,y,state] d[x,y,state] 看成一个点,两种状态转移方式看成这样两种边:
- d [ x , y , s t a t e ] d[x,y,state] d[x,y,state] 向 d [ x , y , s t a t e o r k e y ] d[x,y,state\ or\ key] d[x,y,state or key] 连一条长度为 0 0 0 的边
- d [ x , y , s t a t e ] d[x,y,state] d[x,y,state] 向 d [ a , b , s t a t e ] d[a,b,state] d[a,b,state] 连一条长度为 1 1 1 的边
整个问题就转化为在这样的图上求从起点走到任意一个终点 d [ n , m , 0 ~ 2 p − 1 ] d[n, m, 0~2^p-1] d[n,m,0~2p−1] 的最短距离。由于图中边权只有 0 0 0 和 1 1 1,可以用双端队列BFS求解。
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
#include <set>
#define x first
#define y second
using namespace std;
typedef pair <int, int> PII;
const int N = 11, M = N * N, E = 400, P = 1 << 10;
int n, m, p, k;
int h[M], e[E], w[E], ne[E], idx;
int g[N][N], key[M];
int dist[M][P];
bool st[M][P];
set <PII> edges;
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void build(){
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
for (int u = 0; u < 4; u ++){
int x = i + dx[u], y = j + dy[u];
if (!x || x > n || !y || y > m) continue;
int a = g[i][j], b = g[x][y];
if (edges.count({a, b}) == 0) add(a, b, 0);
}
}
int bfs(){
memset(dist, 0x3f, sizeof dist);
dist[1][0] = 0;
deque <PII> q;
q.push_back({1, 0});
while (!q.empty()){
PII t = q.front();
q.pop_front();
if (st[t.x][t.y]) continue;
st[t.x][t.y] = 1;
if (t.x == n * m) return dist[t.x][t.y];
if (key[t.x]){
int state = t.y | key[t.x];
if (dist[t.x][state] > dist[t.x][t.y]){
dist[t.x][state] = dist[t.x][t.y];
q.push_front({t.x, state});
}
}
for (int i = h[t.x]; ~i; i = ne[i]){
int j = e[i];
if (w[i] && !(t.y >> (w[i] - 1) & 1)) continue;
if (dist[j][t.y] > dist[t.x][t.y] + 1){
dist[j][t.y] = dist[t.x][t.y] + 1;
q.push_back({j, t.y});
}
}
}
return -1;
}
int main(){
cin >> n >> m >> p >> k;
for (int i = 1, t = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++, t ++)
g[i][j] = t;
memset(h, -1, sizeof h);
while (k --){
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
edges.insert({a, b}), edges.insert({b, a});
if (c) add(a, b, c), add(b, a, c);
}
build();
int s;
cin >> s;
while (s --){
int x, y, id;
cin >> x >> y >> id;
key[g[x][y]] |= 1 << (id - 1);
}
cout << bfs() << endl;
return 0;
}
三、最短路计数
回顾DP问题中求方案数的方法:
设
f
[
i
]
f[i]
f[i] 是所求值,
c
n
t
[
i
]
cnt[i]
cnt[i] 是其方案数,将
f
[
i
]
f[i]
f[i] 代表的集合划分成多个子集
s
1
,
s
2
,
.
.
.
,
s
n
s_1,s_2,...,s_n
s1,s2,...,sn,先用状态转移方程求出
f
[
i
]
f[i]
f[i] 的值,再在集合划分中分别求出哪些子集
s
k
s_k
sk 满足
f
[
s
k
]
+
w
k
=
f
[
i
]
f[s_k]+w_k=f[i]
f[sk]+wk=f[i],其中
w
k
w_k
wk 指从
s
k
s_k
sk 转移至
i
i
i 的花费,这些子集的方案数之和
∑
c
n
t
[
s
k
]
\sum cnt[s_k]
∑cnt[sk] 即为
c
n
t
[
i
]
cnt[i]
cnt[i]。
求最短路的数量完全可以依照上述方法,在计算到点 u u u 的最短路数量 c n t [ u ] cnt[u] cnt[u] 时,先求出到点 u u u 的最短距离 d i s t [ u ] dist[u] dist[u],再枚举所有与 u u u 连边的点 i i i,边权为 w w w,判断 d i s t [ i ] + w dist[i]+w dist[i]+w 是否等于 d i s t [ u ] dist[u] dist[u],若等于则 c n t [ u ] cnt[u] cnt[u] 中加上 c n t [ i ] cnt[i] cnt[i]。
但是不同于DP,最短路中不一定存在状态之间的拓扑序,即在枚举所有与 u u u 连边的点 i i i 时, d i s t [ i ] dist[i] dist[i] 与 c n t [ i ] cnt[i] cnt[i] 还未被计算过。如果状态之间没有拓扑序,就不存在一个“入口”,任何状态都无法被计算出来。为此,我们需要建立出一个拓扑序。
引入最短路树 (最短路拓扑图) 的概念:在求最短路时,记录每个点的最短路是由哪个点更新的,若同时有多个点,只记录其中的一个。在所有点的最短路都确定之后,将每个点记录下的前驱视作它的父节点,这样就得到了一棵以起点为根节点的树,而树是具有拓扑序的。只要按照这棵树上的顺序就可以求出到达每个点的最短路径的数量。
求最短路有三种情况:
- 无边权 (边权相等且为正),用BFS:一层一层扩展,每个点入队一次、出队一次,出队的顺序具有拓扑序
- 正权边,用Dijkstra:每个点第一次出队时最短路被确定,第一次出队的序列具有拓扑序
以上两种方法均可以在求最短路的同时更新最短路径数量 - 任意边权,用SPFA:本身不具备拓扑序,需要先跑一遍最短路,再枚举所有边,建立出最短路径树
本题是BFS求最短路数量,在求最短路同时可以更新数量。代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10, M = 4e5 + 10, mod = 100003;
int n, m;
int h[N], e[M], ne[M], idx;
int dist[N], cnt[N];
int q[N];
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void bfs(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0, cnt[1] = 1;
int hh = 0, tt = 0;
q[0] = 1;
while (hh <= tt){
int t = q[hh ++];
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[t] + 1){
dist[j] = dist[t] + 1;
cnt[j] = cnt[t];
q[++ tt] = j;
}
else if (dist[j] == dist[t] + 1)
cnt[j] = (cnt[j] + cnt[t]) % mod;
}
}
}
int main(){
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while (m --){
int a, b;
scanf("%d %d", &a, &b);
add(a, b), add(b, a);
}
bfs();
for (int i = 1; i <= n; i ++)
printf("%d\n", cnt[i]);
return 0;
}
四、观光
本题需要求出最短路径的数量加上比最短路长度多一的路径数量。如何记录后者?可以改变记录的对象,记录次短路径的数量,仅当次短路长度等于最短路长度加一时,将其算入答案即可。
类似拯救大兵瑞恩一题的思想,我们增加状态的维数,设 d [ i , 0 ] d[i,0] d[i,0] 表示从 1 1 1 到 i i i 的最短路径, d [ i , 1 ] d[i,1] d[i,1] 表示从 1 1 1 到 i i i 的次短路径, c n t [ i , 0 ] , c n t [ i , 1 ] cnt[i,0],cnt[i,1] cnt[i,0],cnt[i,1] 分别表示对应路径的数量。重点是 d [ i , 1 ] d[i,1] d[i,1] 和 c n t [ i , 1 ] cnt[i,1] cnt[i,1] 怎么求。
在Dijkstra时,将 d [ i , 0 ] d[i,0] d[i,0] 和 d [ i , 1 ] d[i,1] d[i,1] 看成两个不同的点。每次取出堆顶元素 { v e r , t y p e , d i s t a n c e } \{ver,type,distance\} {ver,type,distance},分别代表点、点的类型 (即最短路还是次短路),到起点的距离。枚举与其直接有边相连的所有点 j j j,判断能否更新到 j j j 的最短路和次短路,有以下四种情况:
- d [ j , 0 ] > d i s t a n c e + w d[j,0]>distance+w d[j,0]>distance+w,可以更新 j j j 的最短路,同时原本的最短路变为次短路
- d [ j , 0 ] = d i s t a n c e + w d[j,0]=distance+w d[j,0]=distance+w,更新最短路数量 c n t [ j , 0 ] cnt[j,0] cnt[j,0]
- d [ j , 1 ] > d i s t a n c e + w d[j,1]>distance+w d[j,1]>distance+w,可以更新 j j j 的次短路
- d [ j , 1 ] = d i s t a n c e + w d[j,1]=distance+w d[j,1]=distance+w,更新次短路数量 c n t [ j , 1 ] cnt[j,1] cnt[j,1]
这样就可以保证次短路距离也是从小到大依次求出的,满足拓扑序。
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1010, M = 10010;
struct Ver{
int ver, type, dist;
bool operator > (const Ver &W) const{ //小根堆要重载大于号
return dist > W.dist;
}
};
int n, m, S, T;
int h[N], e[M], w[M], ne[M], idx;
int dist[N][2], cnt[N][2];
bool st[N][2];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int dijkstra(){
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, 0x3f, sizeof dist);
dist[S][0] = 0, cnt[S][0] = 1;
priority_queue<Ver, vector<Ver>, greater<Ver>> heap;
heap.push({S, 0, 0});
while (!heap.empty()){
Ver t = heap.top();
heap.pop();
int ver = t.ver, type = t.type, distance = t.dist, count = cnt[ver][type];
if (st[ver][type]) continue;
st[ver][type] = 1;
for (int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if (dist[j][0] > distance + w[i]){
dist[j][1] = dist[j][0], cnt[j][1] = cnt[j][0]; //先将原本的最短路变为次短路
heap.push({j, 1, dist[j][1]}); //只要更新了就要入队
dist[j][0] = distance + w[i], cnt[j][0] = count; //再更新最短路
heap.push({j, 0, dist[j][0]});
}
else if (dist[j][0] == distance + w[i]) cnt[j][0] += count;
else if (dist[j][1] > distance + w[i]){
dist[j][1] = distance + w[i], cnt[j][1] = count;
heap.push({j, 1, dist[j][1]});
}
else if (dist[j][1] == distance + w[i]) cnt[j][1] += count;
}
}
int res = cnt[T][0];
if (dist[T][0] + 1 == dist[T][1]) res += cnt[T][1];
return res;
}
int main(){
int cases;
scanf("%d", &cases);
while (cases --){
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
idx = 0;
while (m --){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
scanf("%d %d", &S, &T);
printf("%d\n", dijkstra());
}
return 0;
}