【图论杂题】Roads and Planes G 详细题解~~
[USACO11JAN] Roads and Planes G
题目:道路与航线
来源于洛谷 P3008 [USACO11JAN] Roads and Planes G
题面描述
Farmer John 正在一个新的销售区域对他的牛奶销售方案进行调查。他想把牛奶送到 T T T 个城镇 ( 1 ≤ T ≤ 2.5 × 1 0 4 1 \le T \le 2.5\times 10^4 1≤T≤2.5×104 ),编号为 1 1 1 到 T T T 。这些城镇之间通过 R R R 条道路 ( 1 ≤ R ≤ 5 × 1 0 4 1 \le R \le 5 \times 10^4 1≤R≤5×104 ,编号为 1 1 1 到 R R R ) 和 P P P 条航线 ( 1 ≤ P ≤ 5 × 1 0 4 1 \le P \le 5 \times 10^4 1≤P≤5×104 ,编号为 1 1 1 到 P P P ) 连接。每条道路 i i i 或者航线 i i i 连接城镇 A i A_i Ai ( 1 ≤ A i ≤ T 1 \le A_i \le T 1≤Ai≤T )到 B i B_i Bi ( 1 ≤ B i ≤ T 1 \le B_i \le T 1≤Bi≤T ),花费为 C i C_i Ci 。
对于道路 0 ≤ C i ≤ 1 0 4 0 \le C_i \le 10^4 0≤Ci≤104 ;然而航线的花费很神奇,花费 C i C_i Ci 可能是负数( − 1 4 ≤ C i ≤ 1 4 -1^4 \le C_i \le 1^4 −14≤Ci≤14 )。道路是双向的,可以从 A i A_i Ai 到 B i B_i Bi,也可以从 B i B_i Bi 到 A i A_i Ai ,花费都是 C i C_i Ci 。然而航线与之不同,只可以从 A i A_i Ai 到 B i B_i Bi 。
事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台 了一些政策保证:如果有一条航线可以从 A i A_i Ai 到 B i B_i Bi,那么保证不可能通过一些道路和航线从 B i B_i Bi 回到 A i A_i Ai 。由于 F J FJ FJ 的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。他想找到从发送中心城镇 S S S ( 1 ≤ S ≤ T 1 \le S \le T 1≤S≤T) 把奶牛送到每个城镇的最便宜的方案,或者知道这是不可能的。
输入格式
共 R + P + 1 R+P+1 R+P+1 行
第 1 1 1 行:四个整数 T T T , R R R , P P P 和 S S S ,分别表示城镇的数量,道路的数量,航线的数量和中心城镇。
第 2 2 2 到 R + 1 R+1 R+1 行:每行三个整数 A i A_i Ai , B i B_i Bi 和 C i C_i Ci ,描述一条道路。
第 R + 2 R+2 R+2 到 R + P + 1 R+P+1 R+P+1 行:每行三个整数 A i A_i Ai , B i B_i Bi 和 C i C_i Ci ,描述一条航线。
输出格式
共
T
T
T 行,第
i
i
i 行输出城市
S
S
S 到城市
i
i
i 的最小花费。如果不能到达,输出NO PATH
样例
样例输入 #1
6 3 3 4
1 2 5
3 4 5
5 6 10
3 5 -100
4 6 -100
1 3 -10
样例输出 #1
NO PATH
NO PATH
5
0
-95
-100
说明/提示
6
6
6 个城镇。
1
1
1 号镇和
2
2
2 号镇、
3
3
3 号镇和
4
4
4 号镇、
5
5
5 号镇和
6
6
6 号镇之间有公路,费用分别为
5
、
5
5、5
5、5 和
10
10
10 ;有飞机从
3
3
3 号镇飞往
5
5
5 号镇,从
4
4
4 号镇飞往
6
6
6 号镇,以及从
1
1
1 号镇飞往
3
3
3 号镇,费用分别为
−
100
、
−
100
-100、-100
−100、−100 和
−
10
-10
−10 。FJ总部位于
4
4
4 号镇。
FJ的奶牛从
4
4
4 号镇开始,可以在路上到达
3
3
3 号镇。他们可以从城镇
3
3
3 和
4
4
4 乘坐飞机前往城镇
5
5
5 和
6
6
6 。然而,由于无法前往城镇
1
1
1 和
2
2
2 ,因此无法前往在平面上从
1
1
1 向后移动到
3
3
3 。
题解
流程思路
先浅浅的理解题意,求出从源点 v s v_s vs 出发,到图中每个顶点的最便宜方案。
图中用两种路径,分别是道路和航线:
道路:可从
a
i
a_i
ai 到
b
i
b_i
bi 往返行走,双向通行。
航线:只可从
a
i
a_i
ai 通往
b
i
b_i
bi ,不能返回,单向通行。
观察题目,由于这里的从 a i a_i ai 到 b i b_i bi 的航线中会出现负边权,所以,会导致我们下意识以为不能使用基于贪心思想的 Dijkstra 去解决这个问题,会改用 spfa + SLF ,我觉得这已经很棒了,但是,学校教练出的数据过于优秀,竟然卡爆了,不得不让我们改变思考的方向。
仔细找找,就可以轻易发现这题目中有个漏洞,就是道路中的边权全是正的,所以在处理道路时,我们可以大胆的用 Dijkstra 更新最短路。
好的,道路的问题已经解决了,只剩下航线,不妨用缩点好大法,把所有的道路强连通分量都看做一个顶点,然后用航线进行连接(可能会出现重边,不过无关要紧),如下图:
由于所有道路的连通块已经缩成一个点了,所以本来图中的无向边全部也缩了进去;
再考虑到航线可能会出现负边权,可以想到拓扑序列对航线进行单源最短路,每次从入度为
0
0
0 的连通块出发,每遍历一个连通块,就更新从起始顶点
v
s
v_s
vs 出发,到该连通分量里的所有顶点的最短距离。
代码所使用方法是【迪杰斯特拉】dijkstra、【拓扑排序】topsort 和 DFS 。
-
初始化:
d i d_i di 表示顶点 v i v_i vi 属于的强连通分量的编号;
b l o c k i block_i blocki 存储第 i i i 个连通分量里的顶点;
d e g r e e i degree_i degreei 表示第 i i i 个连通块的入度;
d i s t i dist_i disti 存储从起始点 v s v_s vs 出发到 v i v_i vi 的最短路径;
v e c i , j vec_{i,j} veci,j 表示有一条从 v i v_i vi 通往 v j v_j vj 的单向边 v i → v j v_i \to v_j vi→vj(有可能可以返回(双向,即道路),即 v i ← v j v_i \gets v_j vi←vj ); -
预处理
对所有的所有双向道路都进行 dfs 处理,找出所有连通块,并把其顶点和连通分量计入 d i d_i di 和 b l o c k i block_i blocki 中 ;
对于单向的航线,相当于每个连通块之间的边,所以需要统计出每个连通块缩成一个顶点 i i i 后的入度 d e g r e e i degree_i degreei ;
对于从源点到图中所有的顶点的最短路都赋为无穷大, d i s t i ← ∞ dist_i \gets \infty disti←∞ 。 -
更新最短路
①. 按照拓扑序依次处理每个连通块I. 先将所有入度为 0 0 0 的连通块的编号加入拓扑队列中;
II. 每次从队头取出一个连通块的编号 d d d ;②. 对图中的道路的正权边跑 Dijkstra 算法
提前定义好小根堆的优先队列 h e a p heap heap (即内部从小到大排序)。I. 将该 b l o c k [ d ] block[d] block[d] 中的所有点加入堆中;
II. 每次取出堆中距离最小的点的 u u u ;
III. 然后通过遍历 u u u 的所有相连的边访问邻接点 v v v 。
如果 d [ u ] = d [ v ] d[u]=d[v] d[u]=d[v] (即两个顶点【城镇】都在同一个连通块中,直接加入队列),若 d i s t [ v ] > d i s t [ u ] + d i s t a n c e dist[v]>dist[u]+distance dist[v]>dist[u]+distance ,则更新 d i s t [ v ] ← d i s t [ u ] + d i s t a n c e dist[v] \gets dist[u]+distance dist[v]←dist[u]+distance ;
反则 d [ u ] ≠ d [ v ] d[u] \neq d[v] d[u]=d[v] ,则减入度, − − d e g r e e [ v ] --degree[v] −−degree[v];
如果此时的入度为 0 0 0 了,则可以直接将其插入拓扑序列中。 -
输出
最后直接输出从源点 v s v_s vs 到每个顶点 v i v_i vi 的最短路径 d i s t i dist_i disti 即可;
注意,由于有负边权,所以若 d i s t i ≥ ∞ 2 dist_i \ge \frac{\infty}{2} disti≥2∞ ,则输出NO PATH
。
本蒟蒻自己的小小理解:由于 d i s t i dist_i disti 初始赋值为 ∞ \infty ∞ (正无穷大),而且图中存在负边权,肯定是不能直接判断 d i s t i ≠ ∞ dist_i \neq \infty disti=∞ ,因为在更新最小值时, ∞ + − w v i → v j < ∞ \infty+{-w_{v_i \to v_j}}<\infty ∞+−wvi→vj<∞ ,想想就可以知道正无穷大的一半还是一个很大的数,所以路径尽管上加上负边权,下限还是达不到该数,而且路径的正权值的上限也达不到,很好的解决了在计算含有的负权的图,求最短路中从源点出发是否能到达到该顶点的问题。
Code(Dijlstra+topsort)
#include<bits/stdc++.h>
#define int long long
#define M(x,y) make_pair(x,y)
using namespace std;
typedef pair<int,int> pll;
const int oo=0x3f3f3f3f;
const int N=25010;
int n,r,p,S,x,y,z;
int head[N],idx;
int dist[N],d[N];
int cnt,degree[N];
bool st[N];
queue<int> q;
vector<int> block[N];
vector<pll> vec[N];
priority_queue<pll,vector<pll>,greater<pll> >heap;
void dijkstra(int s) { //Dijkstra处理道路的正权最短路
for(int i=0;i<block[s].size();i++) {
int v=block[s][i];
heap.push({dist[v],v});
}
while(!heap.empty()) {
pll k=heap.top();
heap.pop();
int u=k.second; //取出队顶的顶点u
if(st[u]) continue;
st[u]=1;
for(int i=0;i<vec[u].size();i++) {
int v=vec[u][i].second; //取出顶点u的邻接点v及其边权
int distance=vec[u][i].first;
if(dist[v]>dist[u]+distance) {
dist[v]=dist[u]+distance; //更新最短路
if(d[v]==d[u]) //两个连通块都在同一个连通分量中,加入队列
heap.push(M(dist[v],v));
}
if(d[v]!=d[u]&&--degree[d[v]]==0) //若连通块缩成顶点的入度为0,则加入堆集合
q.push(d[v]);
}
}
return ;
}
void topsort() { //拓扑排序处理航线的负权边最短路
fill(dist,dist+N,oo);
dist[S]=0;
for(int i=1;i<=cnt;i++)
if(!degree[i]) q.push(i);
while(!q.empty()) {
int t=q.front();
q.pop();
dijkstra(t);
}
}
void dfs(int u,int id) { //dfs处理道路的连通块,并且缩点
d[u]=id; //记录属于道路的顶点分别所在的连通块
block[id].push_back(u); //把顶点加入连通块集合中
for(int i=0;i<vec[u].size();i++) { //遍历顶点u的所有邻接点
int v=vec[u][i].second;
if(!d[v]) dfs(v,id); //继续访问下个顶点
}
}
signed main() {
scanf("%lld%lld%lld%lld",&n,&r,&p,&S);
while(r--) {
scanf("%lld%lld%lld",&x,&y,&z);
vec[x].push_back({z,y});
vec[y].push_back({z,x}); //添加道路的无向边
}
for(int i=1;i<=n;i++)
if(!d[i]) dfs(i,++cnt);
while(p--) {
scanf("%lld%lld%lld",&x,&y,&z);
vec[x].push_back({z,y}); //航线的单向通道
degree[d[y]]++; //记录每个连通块的入度
}
topsort();
for(int i=1;i<=n;i++) { //输出从源点出发到顶点i的最短路径
if(dist[i]<=oo/2) printf("%lld\n",dist[i]);
else printf("NO PATH\n");
}
return 0;
}
后记
如有侵权,请联系一下我,说明情况,如属实,我会立即撤回文章!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!