【AcWing算法题高课】3.1.3单源最短路的扩展应用

本节介绍单源最短路的扩展:虚拟源点、分层图、最短路计数、单源次短路。

一、选择最佳线路

1137.选择最佳线路 题目链接

本题中有多个起点,求出从这些起点中的某一个到达终点的最短距离。由于起点只有一个,一种想法是在反向图上求终点到各个起点的最短路,取最小值,但是若终点也有多个时便无法处理,这种方法难以扩展。另一种想法是分别从每个起点跑一遍最短路,取最小值,时间复杂度为 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;
}

二、拯救大兵瑞恩

1131.拯救大兵瑞恩 题目链接

如何处理钥匙和锁着的门?如果仍然用原本的 dist[] 数组存储到达每一个点的最短距离,发现这样是很难转移的——无法判断当前手中有没有钥匙,能不能穿过某扇门。一个直观的想法是类似状态压缩DP,增加状态的维数,用二进制数同时存储当前手中的钥匙。

用闫氏DP分析法:

  1. 状态表示: 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) 属性:最小值
  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] 看成一个点,两种状态转移方式看成这样两种边:

  1. 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 的边
  2. 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,02p1] 的最短距离。由于图中边权只有 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;
}

三、最短路计数

1134.最短路计数 题目链接

回顾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] 还未被计算过。如果状态之间没有拓扑序,就不存在一个“入口”,任何状态都无法被计算出来。为此,我们需要建立出一个拓扑序。

引入最短路树 (最短路拓扑图) 的概念:在求最短路时,记录每个点的最短路是由哪个点更新的,若同时有多个点,只记录其中的一个。在所有点的最短路都确定之后,将每个点记录下的前驱视作它的父节点,这样就得到了一棵以起点为根节点的树,而树是具有拓扑序的。只要按照这棵树上的顺序就可以求出到达每个点的最短路径的数量。

求最短路有三种情况:

  1. 无边权 (边权相等且为正),用BFS:一层一层扩展,每个点入队一次、出队一次,出队的顺序具有拓扑序
  2. 正权边,用Dijkstra:每个点第一次出队时最短路被确定,第一次出队的序列具有拓扑序
    以上两种方法均可以在求最短路的同时更新最短路径数量
  3. 任意边权,用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;
}

四、观光

383.观光 题目链接

本题需要求出最短路径的数量加上比最短路长度多一的路径数量。如何记录后者?可以改变记录的对象,记录次短路径的数量,仅当次短路长度等于最短路长度加一时,将其算入答案即可。

类似拯救大兵瑞恩一题的思想,我们增加状态的维数,设 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 的最短路和次短路,有以下四种情况:

  1. 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 的最短路,同时原本的最短路变为次短路
  2. 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]
  3. 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 的次短路
  4. 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值