道路与航线(算法竞赛进阶指南)

农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。

他想把牛奶送到 T 个城镇,编号为 1∼T。

这些城镇之间通过 R 条道路 (编号为 1 到 R) 和 P 条航线 (编号为 1 到 P) 连接。

每条道路 i 或者航线 i 连接城镇 Ai 到 Bi,花费为 Ci。

对于道路,0≤Ci≤10,000;然而航线的花费很神奇,花费 Ci 可能是负数(−10,000≤Ci≤10,000)。

道路是双向的,可以从 Ai 到 Bi,也可以从 Bi 到 Ai,花费都是 Ci。

然而航线与之不同,只可以从 Ai 到 Bi。

事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策:保证如果有一条航线可以从 Ai 到 Bi,那么保证不可能通过一些道路和航线从 Bi 回到 Ai。

由于约翰的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。

他想找到从发送中心城镇 S 把奶牛送到每个城镇的最便宜的方案。

输入格式

第一行包含四个整数 T,R,P,S。

接下来 R 行,每行包含三个整数(表示一个道路)Ai,Bi,Ci。

接下来 P 行,每行包含三个整数(表示一条航线)Ai,Bi,Ci。

输出格式

第 1..T 行:第 i 行输出从 S 到达城镇 i 的最小花费,如果不存在,则输出 NO PATH。

数据范围

1≤T≤25000,

1≤R,P≤50000,

1≤Ai,Bi,S≤T

输入样例:

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

他想找到从发送中心城镇 S 把奶牛送到每个城镇的最便宜的方案。

 与其他最短路的题有区别。 

这道题我们需要用到拓扑排序

拓扑排序通常用来“排序”具有依赖关系的任务。

  1. 每个顶点出现且只出现一次。
  2. 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

 这道题我们更新最短路时必须满足这个关系。

连通块处理

图中同时有双向边和单向边。我们可以用连通块来表示出这种关系,使遍历更加方便直观。

 个人感觉连通块就是把点分类。

dijkstra

因为是单源最短路。

遍历方向

这道题中我们求的是每个点到起点的距离,所以我们把每个点看成源点,只是统一以出发点为原点进行计算,这样可以保证不漏。

其次,我们为了去重,用上st数组来保证每一个点只能拓展一次,但是可以注意到,一开始我将块内点加入的时候并没有给他们打上vis标记。因此如果他们再次松弛的话还是可以被加入队列。

这样做的原因是因为我们在拓扑排序的基础上,大体上看整个遍历的方向是固定的,不必正着来一遍,再反着来一遍。

由此

 图片来源:AcWing 342. 道路与航线 - AcWing

 代码来源:AcWing 342. 道路与航线 - AcWing

#include<bits/stdc++.h>
using namespace std;

typedef pair<int,int> PII;

const int N = 25010, M = 150010, INF = 0x3f3f3f3f;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool st[N];

int n, mr, mp, s;
//id:点所在的连通块号, bcnt:连通块数量
int id[N], bcnt;
//连通块的入度数组
int deg[N];
vector<int> block[N];

int q[N], hh, tt = -1;

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u)
{
	block[bcnt].push_back(u);
	id[u] = bcnt;
	
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if(!id[j]) dfs(j);
	}
}

void dijkstra(int block_id)
{
	priority_queue<PII, vector<PII>, greater<PII>> heap;
	
	//取出该连通块所有的点, 入堆
	for(int u : block[block_id])
	{
		heap.push({dist[u], u});
	}
	
	while(heap.size())
	{
		PII t = heap.top();
		heap.pop();
		int u = t.second;
		
		if(st[u]) continue;
		st[u] = true;
		
		for(int i = h[u]; i != -1; i = ne[i])
		{
			int j = e[i];
			if(dist[j] > dist[u] + w[i])
			{
				dist[j] = dist[u] + w[i];
				if(id[j] == block_id) heap.push({dist[j], j});
			}
			
			if(id[j] != block_id && --deg[id[j]] == 0) q[++tt] = id[j];
		}
		
	}
}

void topoSort()
{
	//dist赋初值
	memset(dist, 0x3f, sizeof dist);
	dist[s] = 0;
	
	//入度为0的连通块入队列
	for(int i = 1; i <= bcnt; i++)
	{
		if(!deg[i]) q[++tt] = i;
	}
	
	while(hh <= tt)
	{
		int t = q[hh++];
		dijkstra(t);
	}
}

int main()
{
	scanf("%d%d%d%d", &n, &mr, &mp, &s);
	memset(h, -1, sizeof h);
	for(int i = 0; i < mr; i++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}
	
	//建立所有的连通块
	for(int i = 1; i <= n; i++)
	{
		if(!id[i]) {
			++bcnt;
			dfs(i);
		}
	}
	//在连通块之间建边
	for(int i = 0; i < mp; i++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), deg[id[b]]++;
	}
	
	//算法开始
	topoSort();
	
	for(int i = 1; i <= n; i++)
	{
		if(dist[i] > INF / 2) puts("NO PATH");
		else printf("%d\n", dist[i]);
	}
	
	return 0;
}

注意事项:

1.对于所有入度为0的连通块,都要加入队列

2.注意最后判定的时候不是dist == inf时才是不连通。因为这个图存在负边(见给的样例图),inf可能会被负边收敛,但仍然是不连通的,所以改成dist >inf / 2

 这道题解法不限上述一种。作者水平较差,如有纰漏,欢迎指出。

posted @ 2022-09-24 21:16  zyc_xianyu  阅读(31)  评论(0编辑  收藏  举报