道路和航线 题解

题目描述

Farmer John 正在一个新的销售区域对他的牛奶销售方案进行调查。他想把牛奶送到 \(T\) 个城镇 ,编号为 \(1\)\(T\)。这些城镇之间通过 \(R\) 条道路(编号为 \(1\)\(R\))和 \(P\) 条航线(编号为 \(1\)\(P\))连接。每条道路 \(i\) 或者航线 \(i\) 连接城镇 \(A_i\)\(B_i\),花费为 \(C_i\)

对于道路,\(0 \le C_i \le 10^4\),然而航线的花费很神奇,花费 \(C_i\) 可能是负数。道路是双向的,可以从 \(A_i\)\(B_i\),也可以从 \(B_i\)\(A_i\),花费都是 \(C_i\)。然而航线与之不同,只可以从 \(A_i\)\(B_i\)

事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策保证:如果有一条航线可以从 \(A_i\)\(B_i\),那么保证不可能通过一些道路和航线从 \(B_i\) 回到 \(A_i\)。由于 FJ 的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。他想找到从发送中心城镇 \(S\) 把奶牛送到每个城镇的最便宜的方案,或者知道这是不可能的。

输入格式

第一行为四个空格隔开的整数:\(T, R, P,S\)

第二到第 \(R+1\) 行:三个空格隔开的整数(表示一条道路):\(A_i, B_i\)\(C_i\)

\(R+2\)\(R+P+1\) 行:三个空格隔开的整数(表示一条航线):\(A_i, B_i\)\(C_i\)

输出格式

输出 \(T\) 行,第 \(i\) 行表示到达城镇 \(i\) 的最小花费,如果不存在输出 NO PATH

样例输入

6 3 3 4 
1 2 5 
3 4 5 
5 6 10 
3 5 -100 
4 6 -100 
1 3 -10 

样例输出

NO PATH 
NO PATH 
5 
0 
-95 
-100 

样例说明

一共六个城镇。在 \(1\)\(2\)\(3\)\(4\)\(5\)\(6\) 之间有道路,花费分别是 \(5,5,10\)。同时有三条航线:\(3\to 5\)\(4\to 6\)\(1\to 3\),花费分别是 \(-100,-100,-10\)。FJ 的中心城镇在城镇 \(4\)。FJ 的奶牛从 \(4\) 号城镇开始,可以通过道路到达 \(3\) 号城镇。然后他们会通过航线达到 \(5\)\(6\) 号城镇。但是不可能到达 \(1\)\(2\) 号城镇。

数据范围

对于全部数据,\(1\le T\le 2.5\times 10^4,1\le R,P\le 5\times 10^4,1\le A_i,B_i,S\le T\)。保证对于所有道路,\(0 \le C_i \le 10^4\),对于所有航线,\(-10^4 \le C_i \le 10^4\)

分析

题目大意就是求 s 到任一点的最短路,因为图中有负边权,所以优先考虑到用 SPFA。但是不加优化不加快读此题会卡一到两个点。
所以还是老老实实跑 Dijkstra 吧。
很明显单纯的 Dij 跑负边权会出问题,反例也很好找。
但是题目中给了个条件,意思是如果一条边是航线,不仅不存在负权回路,它还会把图分割成两个独立的连通块(也就是说,如果把这条边去掉,它所连的两部分是不连通的)。
如果我们把每个连通块缩成一个点,那么整个图就变成了 DAG,要找起点 s 到每个点的最短路,直接拓扑排序跑一遍就可以。

  • 我们先找出所有的连通块,DFS即可,标记一下每个点属于哪个连通块
  • 加入航线的边,记录每个连通块的入度,跑拓扑用
  • 寻找入度为 0 的连通块加入队列,从队首取出一个连通块编号,对其中所有的点跑 Dij:
    • 如果松弛过程中的邻接点不是本连通块的点,只更新dis值,其所在连通块入度减一;如果入度为 0,将其连通块加入队列。
    • 否则正常跑Dij即可。
// 先放一个spfa的优化版本
void spfa_deque(int now) {
	deque<Node> q;
	bool inque[N] = {};
	memset(dis, 0x3f, sizeof(dis));
	dis[now] = 0;
	q.push_back(Node(now, 0));
	inque[now] = true;
	while (!q.empty()) {
		Node nd = q.front(); q.pop_front();
		now = nd.id;
		inque[now] = false;
		for (int i = head[now]; i; i = e[i].next) {
			int to = e[i].to;
			if (dis[to] > dis[now] + e[i].w) {
				dis[to] = dis[now] + e[i].w;
				if (!inque[to]) {
					if (!q.empty() && dis[to] < q.front().dis) {
						q.push_front(Node(to, dis[to]));
					} else {
						q.push_back(Node(to, dis[to]));
					}
					inque[to] = true;
				}
			}
		}
	}
}
void Dijkstra(int x) { // x表示连通块的编号
	priority_queue<Node> q;
	int size = vct[x].size(); // 连通块中点的个数
	int now;
	// 将连通块x中的所有点放到优先队列
	// 必须要保证能够拿到所有的点去更新其他连通块的入度
	for (int i = 0; i < size; ++i) {
		now = vct[x][i];
		q.push(Node(now, dis[now]));
	}
	while (!q.empty()) { // 正常跑Dij
		Node nd = q.top (); q.pop();
		now = nd.id;
		if (dis[now] != nd.dis) continue;
		for (int i = head[now]; i; i = e[i].next) {
			int to = e[i].to;
			// 如果to不是连通块x中的点,则to所在的连通块的入度减1
			// 如果入度变成0,将to所在的连通块加到拓扑的队列中备用
			if (belong[to] != x) {
				--ru[belong[to]];
				if (ru[belong[to]] == 0) {
					tpque.push(belong[to]);
				}
			}
			// 前面的判断是为了防止负边权造成dis值减小,影响最后的判断是否连通
			// 如果dis[now]==INF,说明起点与它不连通,松弛没有意义
			if (dis[now] < INF && dis[to] > dis[now] + e[i].w) {
				dis[to] = dis[now] + e[i].w;
				if (belong[to] == x) { // 如果是连通块x中的点,再加入到优先队列
					q.push(Node(to, dis[to]));
				}
			}
		}
	}
}

void topsort() {
	for (int i = 1; i <= scc; ++i) { // 找到所有入度为0的连通块加入队列 
		if (ru[i] == 0) {
			tpque.push(i);
		}
	}
	memset(dis, 0x3f, sizeof(dis)); // 初始化Dij的数组跑最短路用
	dis[s] = 0;
	while (!tpque.empty()) {
		int x = tpque.front();
		tpque.pop();
		Dijkstra(x);
	}
}

// 主函数中处理完双向道路之后,调用该函数寻找连通块
// belong数组保存当前结点属于哪个连通块
void dfs(int x) {
	belong[x] = scc;
	vct[scc].push_back(x);
	for (int i = head[x]; i; i = e[i].next) {
		if (!belong[e[i].to]) {
			dfs(e[i].to);
		}
	}
}
// 最后判断如果dis值等于INF,则无法到达,否则输出dis值即可
posted @ 2020-07-09 13:21  狂飙霹雳虎  阅读(353)  评论(0编辑  收藏  举报