【图论杂题】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 1T2.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 1R5×104 ,编号为 1 1 1 R R R ) 和 P P P 条航线 ( 1 ≤ P ≤ 5 × 1 0 4 1 \le P \le 5 \times 10^4 1P5×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 1AiT )到 B i B_i Bi ( 1 ≤ B i ≤ T 1 \le B_i \le T 1BiT ),花费为 C i C_i Ci

对于道路 0 ≤ C i ≤ 1 0 4 0 \le C_i \le 10^4 0Ci104 ;然而航线的花费很神奇,花费 C i C_i Ci 可能是负数( − 1 4 ≤ C i ≤ 1 4 -1^4 \le C_i \le 1^4 14Ci14 )。道路是双向的,可以从 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 1ST) 把奶牛送到每个城镇的最便宜的方案,或者知道这是不可能的。

输入格式

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 55 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 100100 − 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、【拓扑排序】topsortDFS

  • 初始化:
    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 vivj(有可能可以返回(双向,即道路),即 v i ← v j v_i \gets v_j vivj );

  • 预处理
    对所有的所有双向道路都进行 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} disti2 ,则输出 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 +wvivj< ,想想就可以知道正无穷大的一半还是一个很大的数,所以路径尽管上加上负边权下限还是达不到该数,而且路径的正权值的上限达不到,很好的解决了在计算含有的负权,求最短路中从源点出发是否能到达到该顶点的问题。

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;
} 

后记

如有侵权,请联系一下我,说明情况,如属实,我会立即撤回文章!

posted @   Fireworks_Rise  阅读(19)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示