【AcWing算法提高课】3.1.2单源最短路的综合应用

本节介绍单源最短路与其他算法的结合。

一、新年好

1135.新年好 题目链接

最短路与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 1ab...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;
}

二、通信线路

340.通信线路 题目链接

最短路与二分的结合。(最大值最小)

定义在 [ 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 1 N N N 最少经过长度大于 x x x 的边的数量?
    可以将所有边分类,若边长大于 x x x,则边权看成 1 1 1,否则边权看成 0 0 0,用双端队列BFS求改变权值后 1 1 1 N N N 的最短路。
  2. 定义区间为何包含了 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;
}

三、道路与航线

342.道路与航线 题目链接

单源最短路与拓扑排序的结合。

已知本题SPFA会被卡。考虑题目给定的两种路径的特点:

  1. 道路,双向,边权非负
  2. 航线,单向,边权任意,无环,且如果有一条航线从 A A A B B B,则 B B B 无法通过道路和航线回到 A A A

航线单向且无环,想到拓扑图,但是除航线外还有双向的非负权边。由于航线具有的特殊性质,若将所有航线全部去掉后,所有点会形成多个“团状”:团内部的点由双向边连接,团与团之间互不相通 (仅通过航线相连,且航线单向),如下图。

在这里插入图片描述

在每个团内部,用Dijkstra算法求出两点间的最短路,时间复杂度 O ( M log ⁡ N ) O(M\log N) O(MlogN);在团与团之间,按线性时间复杂度的拓扑序扫描。

具体实现步骤:

  1. 先输入所有双向道路,然后DFS出所有连通块,计算两个数组:id[] 存储每个点属于哪个连通块;vector <int> block[] 存储每个连通块里有哪些点;
  2. 输入所有航线,同时统计每个连通块的入度;
  3. 按照拓扑序依次处理每个连通块,先将所有入度为零的连通块的编号加入队列中;
  4. 每次从队头取出一个连通块的编号 bid
  5. block[id] 中的所有点加入堆中,然后对堆中所有点跑Dijkstra算法;
  6. 每次取出堆中距离最小的点 ver,遍历其所有邻点 j
  7. 如果 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;
}

四、最优贸易

341.最优贸易 题目链接

最短路与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[i1,j],f[i1,jvi]+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 N1 次,每次用三角不等式更新所有点。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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值