【算法提高】单源最短路的建图方式
最短路问题
最短路问题指的是从图中的点出发,求出该点到其他点的最短距离(边上权值之和最小)的路径
其中最短路问题分为两种:
- 单源最短路问题
- 多源汇最短路问题
而我们本章的重点在于单源最短路的建图方式
在了解本章内容之前,建议熟悉之前的单源最短路问题的四种算法:单源最短路模板
为什么要学习单源最短路的建图方式呢?
- 单源最短路的模型很少(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;
}