当前位置: 首页 > news >正文

【算法提高】单源最短路的建图方式

最短路问题

最短路问题指的是从图中的点出发,求出该点到其他点的最短距离(边上权值之和最小)的路径

其中最短路问题分为两种:

  • 单源最短路问题
  • 多源汇最短路问题

而我们本章的重点在于单源最短路的建图方式

在了解本章内容之前,建议熟悉之前的单源最短路问题的四种算法:单源最短路模板

为什么要学习单源最短路的建图方式呢?

  • 单源最短路的模型很少(4个)
  • 在解决最短路问题时,对于算法模板是基本功,而最短路问题的重点也不在算法模板,重点是能把一个实际问题转化为最短路问题的能力,即我们所说的建图能力

接下来开启本章正式内容:

热浪

题目解析

 建图

这题是比较裸最短路的一个模型,我们将城镇想象成图上的点,把双向道路想象成图的边,那么我们可以根据如下测试用例画出图 

 

最短路算法选择 

根据数据范围

  • 点的个数为2500
  • 边的个数为6200
  • 正权图

朴素dijkstra算法时间复杂度:2500*2500 = 6250000 (能过)

堆优化dijkstra算法时间复杂度:6200*log(2500) ≈ 6200 * 10 = 62000 (能过)

不考虑bellman_ford算法,因为spfa的时间复杂度最坏情况下也就是退化成bellman_ford,没有特殊情况不会用到bellman_ford

spfa算法时间复杂度:平均6200,若卡spfa则退化为6200*2500 = 15500000 (能过)

floyd算法时间复杂度:2500*2500*2500 = 15625000000(不能过)

所以前三种单源最短路算法都能过,喜欢哪个写哪个即可

由于第一题,所以代码中我把三种算法的算法模板都写了

代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 2510 , M = 6200 * 2 + 10;
int h[N] , w[M] , ne[M] , e[M] , idx;
int n , m , S , T;
int dist[N];
queue<int> q;
bool st[N];
void add(int a , int b , int c)
{e[idx] = b , ne[idx] = h[a] , w[idx] = c , h[a] = idx++;
}void spfa()
{memset(dist,0x3f,sizeof dist);st[S] = true;dist[S] = 0;q.push(S);while(q.size()){int t = q.front(); q.pop();st[t] = false;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]){st[j] = true;q.push(j);}}}}
}
void dijkstra1()
{//朴素版本dijkstramemset(dist,0x3f,sizeof dist);dist[S] = 0;for(int i = 0 ; i < n - 1 ; ++i){int t = -1;for(int j = 1 ; j <= n ; ++j)if(!st[j] && (t == -1 || dist[j] < dist[t])) t = j;st[t] = true;for(int j = h[t] ; ~j ; j = ne[j]){int k = e[j];dist[k] = min(dist[k] , dist[t] + w[j]);}}
}
void dijkstra2()
{//堆优化版dijkstramemset(dist,0x3f,sizeof dist);priority_queue<PII,vector<PII>,greater<PII>> pq;pq.push({0,S});dist[S] = 0;while(pq.size()){auto t = pq.top();pq.pop();int distance = t.first , ver = t.second;if(st[ver]) continue;st[ver] = true;for(int i = h[ver] ; ~i ; i = ne[i]){int j = e[i];if(!st[j]){dist[j] = min(dist[j] , distance + w[i]);pq.push({dist[j],j});}}}
}
int main()
{cin >> n >> m >> S >> T;memset(h,-1,sizeof h);for(int i = 0 ; i < m ; ++i){int a , b , c;cin >> a >> b >> c;add(a,b,c) , add(b,a,c);}//spfa();//dijkstra1();dijkstra2();cout << dist[T] << endl;return 0;
}

信使

题目解析 

建图 

我们将哨所想象成图的点,将通信线路想象成图的边(无向),可以根据测试用例得出如下图:

首先观察信使送信完成的最短路径有哪些:

  • 从1送到2送到4送到3,总路程11
  • 从1送到2送到3和4,总路程11

假设此时3<->4这条边的权值为100,那么送信完成的最短路就是上述第二条

由于是单源起点(从1出发),那么最短送信完成的时间就是从1号点出发到达其他所有点的最短路中的最大值,同时还需观察1号点是否与其他所有点连通,若有一个点不连通则送信一定无法完成

故步骤为:

  • 先求出1号点到其他点的最短路
  • 统计1号点到其他点的最短路的最大值,该值即为送信完成的最短时间,同时判断是否有点不连通

最短路算法选择 

考虑数据范围:

  • 点的个数为100
  • 边的个数为200
  • 正权图 

朴素dijkstra时间复杂度:100*100 = 10000 (能过)

堆优化dijkstra时间复杂度:200*log100 = 1200 (能过)

spfa时间复杂度:200(能过)

floyd时间复杂度:100*100*100 = 1000000 (能过)

由于前面题目没写过floyd,如下代码使用floyd算法模板

floyd代码 

 注意:floyd初始化时必须把从i点到i点的距离初始化为0,否则如下图会出现错误:

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
int n , m;
int dist[N][N];int main()
{cin >> n >> m;memset(dist,0x3f,sizeof dist);for(int i = 0 ; i < m ; ++i){int a , b , c;cin >> a >> b >> c;dist[a][b] = dist[b][a] = min(dist[a][b] , c);}for(int k = 1 ; k <= n ; ++k)for(int i = 1 ; i <= n ; ++i)for(int j = 1 ; j <= n ; ++j)dist[i][j] = min(dist[i][j] , dist[i][k] + dist[k][j]);int res = 0;for(int i = 1 ; i <= n ; ++i)res = max(res , dist[1][i]);if(res == 0x3f3f3f3f) cout << "-1" << endl;else cout << res << endl;return 0;
}

香甜的黄油

题目解析

建图

我们将牧场想象成图中的点,将牧场相连的道路想象成图中的边,根据测试用例,我们可以建出如下图:

题目要求我们找出使所有牛到达的路程和最短的牧场,所以我们需要额外保存牛和牧场之间的映射关系。

我们遍历所有的点,以该点作为起始点求一次到其他点的最短路。求出来后,我们遍历映射数组,计算每头牛到起始点的距离取和并返回。

最后的结果就是所有点作为起始点返回结果的最小值

最短路算法选择

观察数据范围:

  • 点数:800
  • 边数:1450

朴素dijkstra时间复杂度:800*800*800 = 512000000 (超时)

堆优化dijkstra时间复杂度:1450*log800*800 ≈ 11600000(能过)

spfa时间复杂度:1160000,极端情况:1450*800*800 =  928000000 (能不能过看出题人心情,实测这题可过)

floyd时间复杂度:512000000(超时)

spfa代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 810 , M = 1450*2+10 , INF = 0x3f3f3f3f;
int id[N]; //牛:牧场 映射关系表
int p,n,m;
int h[N] , ne[M] , e[M] , w[M] , idx; //邻接表相关
int dist[N];
bool st[N];
void add(int a , int b , int c)
{e[idx] = b , ne[idx] = h[a] , w[idx] = c , h[a] = idx++;
}
int spfa(int S)
{//S是起点queue<int> q;memset(dist,0x3f,sizeof dist);dist[S] = 0;q.push(S);st[S] = true;while(q.size()){auto t = q.front();q.pop();st[t] = false;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]){st[j] = true;q.push(j);}}}}int sum = 0;for(int i = 0 ; i < p ; ++i){if(dist[id[i]] == INF) return INF;else sum += dist[id[i]];}return sum;
}
int main()
{memset(h,-1,sizeof h);cin >> p >> n >> m;for(int i = 0 ; i < p ; ++i)cin >> id[i];for(int i = 0 ; i < m ; ++i){int a , b , c;cin >> a >> b >> c;add(a,b,c) , add(b,a,c);}int res = INF;for(int i = 1 ; i <= n ; i++)res = min(res , spfa(i));cout << res << endl;return 0;
}

最小花费

题目解析 

建图 

我们可以想象图中的每个点都是一个人,那么边是什么呢?

我们需要进一步转化,设A点发送的钱数为Pa,设每条边的权重不再是手续费,而是(1-手续费)%,对于该值设为wi,那么可以得出如下公式:

  • Pa*w1*w2*....*wn = 100

题目要求Pa是最小的,那么相应的我们要找到w1*w2...*wn最大才行

即找出一条路径,使得该路径的(1-手续费)的乘积是最大的。

我们对w的乘积取一个log:

  • log(w1*w2*...*wn) = log(w1) + log(w2) + ... + log(wn) 

由于log是单调函数,所以就是找到n条边的权值总和最大即可

由于0<w<1,所以log(w)的值是小于0的,所以该图是一个负权图,我们对每个w乘一个负一就能转化为正权图。但乘完一个负一以后,我们就由找出n条边的权值总和最大值转化成了找出n条边的权值总和为最小值,也就转化成了我们一般的单源正权最短路问题

注意:上述的一顿操作都是为了方便理解,实际上我们做的时候只需要求出w1...wn的乘积最大值即可

设每个人为一个点,而(1-手续费)/100为边,那么我们根据测试用例生成一张图:

 

 最短路算法选择

考虑数据范围:

  • 点数:2000
  • 边数:100000 

朴素版dijkstra时间复杂度:2000*2000 = 4000000 (能过)

堆优化dijkstra时间复杂度:100000*log2000 ≈ 1000000 (能过)

spfa时间复杂度:100000,极端情况:2000*100000 = 200000000 (出题人心情好就能过)

floyd时间复杂度:2000*2000*2000 = 8000000000(超时) 

最短路的三种算法都能过,随便选一个就行,我选的是堆优化dijkstra算法

堆优化dijkstra代码 

#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 2010 , M = 100010 * 2;
int h[N] , ne[M] , e[M] , idx;
double w[M] , dist[N];
bool st[N];
int n , m;
int S , E;
double dijkstra()
{priority_queue<pair<double,int>> pq;pq.push({1,S});dist[S] = 1;while(pq.size()){auto t = pq.top();pq.pop();double distance = t.first;int ver = t.second;if(st[ver]) continue;st[ver] = true;for(int i = h[ver] ; ~i ; i = ne[i]){int j = e[i];if(!st[j]){dist[j] = max(dist[j] , distance * w[i]);pq.push({dist[j] , j});}}}return dist[E];
}
void add(int a , int b , int c)
{e[idx] = b , ne[idx] = h[a], w[idx] = (100.0-c)/100 , h[a] = idx++;
}int main()
{cin >> n >> m;memset(h,-1,sizeof h);for(int i = 0 ; i < m ; ++i){int a , b , c;cin >> a >> b >> c;add(a,b,c) , add(b,a,c);}cin >> S >> E;printf("%.8lf\n",100.0 / dijkstra());return 0;
}

最优乘车

题目解析 

注意:求的是最优换乘次数,第一次坐车的时候不算换乘,所以在求出来结果时我们需要对结果减一,但同时如果求得是从1号点到1号点的距离的话,那么会得出来个负数,所以我们需要max(res-1,0)

建图 

我们可以设每一个站点都是图中的一个点,而边指的是是否属于同一趟车能到达的点,由于坐车时不能倒着走,例如一辆车从1号点到2号点,那么你不能再从2号点回到1号点,所以该图是一个有向图,根据如下测试用例,我们可以生成一张图:

由于图有点乱,简要介绍一下建图并用测试用例模拟一下:

图中的边表示的是能否只坐一辆车从A点到达B点,假设给的一行输入是A B,那么表示能坐一辆车先到A点再到B点,也就是A有一条指向B的边,而B没有指向A的边,因为不能倒着走

根据测试用例,从1号点走到7号点,观察图中1号点有一条指向3的边,3号点通过换乘能到达6号点,6号点通过换乘能到7号点,由于第一次坐不算换乘次数,所以换乘次数为2

故当建好图以后,只需要从1号点跑一遍最短路算法即可求出解

最短路算法选择

注意:这题比较新鲜,由于边表示是否能直接坐一辆车到达另一个点,所以边的权值都为1,对于固定权值的图来说,我们可以使用bfs来求解。 

考虑数据范围:

  • 点的个数:500
  • 边的个数:粗略估计一下大约25000左右

bfs时间复杂度:25000(能过)

朴素dijkstra时间复杂度:25000(能过)

堆优化dijkstra时间复杂度:12500(能过)

spfa时间复杂度:25000,极端情况下:12500000(能过)

floyd时间复杂度:125000000(超时)

这题我们采用bfs写一下

bfs代码

#include <iostream>
#include <sstream>
#include <queue>
#include <cstring>
const int N = 510;
using namespace std;
int m , n;
bool g[N][N];
int dist[N];
bool st[N];
void bfs()
{memset(dist,0x3f,sizeof dist);queue<int> q;q.push(1);dist[1] = 0;while(q.size()){auto t = q.front();q.pop();if(st[t]) continue;st[t] = true;for(int i = 1 ; i <= n ; ++i){if(!st[i] && g[t][i]){dist[i] = min(dist[i] , dist[t] + 1) ;q.push(i);}}}
}
int main()
{cin >> m >> n;string line;getline(cin , line);for(int i = 0 ; i < m ; ++i){getline(cin,line);stringstream ss(line);int cnt[N];int p = 0;while(ss >> cnt[p]) p++;for(int j = 0 ; j < p ; ++j)for(int k = j + 1 ; k < p ; ++k)g[cnt[j]][cnt[k]] = true;}bfs();if(dist[n] == 0x3f3f3f3f) cout << "NO" << endl;else cout << max(dist[n] - 1 , 0) << endl;return 0;
}

昂贵的聘礼

题目解析 

 

题目较复杂,简单来说就是我们需要获取1号物品,而1号物品可以直接花金币购买,也可以通过物品+金币的方式进行购买。

对于每个物品来说都是如此,都是可以直接花金币进行购买或者通过其他物品+金币的方式进行购买

建图 

题目中有等级限制,我们先不考虑等级限制。

我们假设每个物品都是图中的一个点,花费的金币为边的权值。那我们需要分为两种情况:

  • 直接花金币进行购买
  • 物品+金币的方式进行购买

为了统一处理,我们需要把直接花金币购买的这种情况转化为物品+金币的方式。

其实物品+金币的方式指的是这两个物品之间有一条边,边的权值就是金币

假设1号物品需要由2号物品+5000金币进行购买,那么指的就是2号物品有一条边指向1号物品,边的权值即为5000

了解了图的含义,在建图时我们可以预设一个虚拟源点,这个虚拟源点能连到其他所有的物品,表示的是直接花金币购买物品的情况,我们设0号点为虚拟源点,真实点从1开始

根据如上分析,参考测试用例,我们可以生成如下图(暂不考虑等级):

 

 

当建好图以后,最终结果就是从0号物品到1号物品的最短路距离,因为不管你如何用别的物品进行交换,最后也一定会走到一个无法交换的点只能直接购买,即表示从0号点到该点的边 

接下来的问题就到了等级限制的问题

当两点的等级之差超过m时,意味着这两点一定不是连通的,求最短路时这两点一定是互不可达的

题目中等级限制之差最多会到100,即最低等级为1,最高等级为100,数据范围不大,如下等级线段

由于我们需要能获得1号物品,所以我们枚举每一个包含1号物品等级且长度为m的区间。

每一次枚举的区间都做一次最短路算法,当等级不在范围中时不在最短路算法的考虑范围之内。

最短路算法的返回值为从0号点到1号点的最短距离(考虑的是等级在范围内的点),最后取一个min就是最终的解

最短路算法选择

考虑数据范围:

  • 点的个数为100
  • 边的个数极端情况下大约为10000

朴素dijkstra算法:1000000(能过)

堆优化dijkstra算法:大约为6000000(能过)

spfa算法:1000000,若卡spfa则为100*10000*100 = 100000000(看出题人心情)

floyd算法解不出来,因为要动态控制等级区间

该代码使用朴素dijkstra实现

朴素dijkstra代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110 , INF = 0x3f3f3f3f;
int m , n;
int g[N][N];
int level[N];
int dist[N];
bool st[N];
int dijkstra(int down , int up)
{memset(dist,0x3f,sizeof dist);memset(st,0x3f,sizeof st);dist[0] = 0;for(int i = 0 ; i < n ; ++i){int t = -1;for(int j = 0 ; j <= n ; ++j)if(!st[j] && (t == -1 || dist[t] > dist[j]))t = j;if(t == -1) return INF;st[t] = true;for(int j = 0 ; j <= n ; ++j)if(level[j] >= down && level[j] <= up)    dist[j] = min(dist[j] , dist[t] + g[t][j]);}return dist[1];
}int main()
{cin >> m >> n;memset(g,0x3f,sizeof g);for(int i = 0 ; i <= n ; ++i)g[i][i] = 0; for(int i = 1 ; i <= n ; ++i){int price , cnt;cin >> price >> level[i] >> cnt;g[0][i] = price;for(int j = 0 ; j < cnt ; ++j){int index;cin >> index >> price;g[index][i] = min(g[index][i] , price);}}int res = 1e9;for(int i = level[1] - m ; i <= level[1] ; ++i)res = min(res , dijkstra(i,i+m));cout << res << endl;return 0;
}

相关文章:

  • Linux系统编程---孤儿进程与僵尸进程
  • UML统一建模
  • Vue常用指令入门
  • 【项目实训个人博客】数据集搜集
  • 【python】尾部多写个逗号会把表达式变成 tuple
  • 使用virtualbox的HostOnly建立共享网络-实现虚拟机上网
  • 面向对象编程的四大特性详解:封装、继承、多态与抽象
  • React 自定义Hook之usePrevious
  • 数字孪生废气处理工艺流程
  • ES6 第一讲 变量定义 堆与栈 字符串的扩展和数值型的扩展
  • 【读书笔记·VLSI电路设计方法解密】问题64:什么是芯片的功耗分析
  • 【C++基本算法】背包问题——完全背包
  • 大数定理(LLN)习题集 · 答案与解析篇
  • 慧通编程——k的幂(课程7)
  • jQuery介绍+使用jQuery实现音乐播放
  • Spring中Bean的作用域和生命周期
  • 51c大模型~合集119
  • win11离线安装donet3.5
  • Linux中的信号量
  • 【python】deepcopy深拷贝浅拷贝(结合例子理解)
  • 世界读书日丨阅读与行走,都是理解世界的方式
  • 翁东华卸任文和友小龙虾公司董事,此前抢镜“甲亢哥”惹争议
  • 年近九旬的迪图瓦,指挥能量比盛年更为强劲
  • 外交部:中企在中韩暂定水域建立渔业养殖设施不违反中韩有关协定
  • “雷公”起诉人贩子王浩文案将开庭:索赔6元,“讨个公道”
  • 纪念沈渭滨︱沈渭滨先生与新修《清史》