题目地址:
https://www.acwing.com/problem/content/344/
农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。他想把牛奶送到 T T T个城镇,编号为 1 ∼ T 1∼T 1∼T。这些城镇之间通过 R R R条道路(编号为 1 1 1到 R R R)和 P P P条航线(编号为 1 1 1到 P P P)连接。每条道路 i i i或者航线 i i i连接城镇 A i A_i Ai到 B i B_i Bi,花费为 C i C_i Ci。对于道路, 0 ≤ C i ≤ 10000 0≤C_i≤10000 0≤Ci≤10000;然而航线的花费很神奇,花费 C i C_i Ci可能是负数( − 10000 ≤ C i ≤ 10000 −10000≤C_i≤10000 −10000≤Ci≤10000)。道路是双向的,可以从 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。由于约翰的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。他想找到从发送中心城镇 S S S把奶牛送到每个城镇的最便宜的方案。
输入格式:
第一行包含四个整数
T
,
R
,
P
,
S
T,R,P,S
T,R,P,S。接下来
R
R
R行,每行包含三个整数(表示一个道路)
A
i
,
B
i
,
C
i
A_i,B_i,C_i
Ai,Bi,Ci。接下来
P
P
P行,每行包含三个整数(表示一条航线)
A
i
,
B
i
,
C
i
A_i,B_i,C_i
Ai,Bi,Ci。
输出格式:
第
1
,
.
.
.
,
T
1,...,T
1,...,T行:第
i
i
i行输出从
S
S
S到达城镇
i
i
i的最小花费,如果不存在,则输出NO PATH
。
数据范围:
1
≤
T
≤
25000
1≤T≤25000
1≤T≤25000
1
≤
R
,
P
≤
50000
1≤R,P≤50000
1≤R,P≤50000
1
≤
A
i
,
B
i
,
S
≤
T
1≤A_i,B_i,S≤T
1≤Ai,Bi,S≤T
由题意可知,整个图可以分为若干个纯由道路连通的连通块(称为”道路连通块“),然后这些连通块互相之间可能有航线。由于如果从某点到另一个点有航线,那么可知这两个点所在的两个道路连通块不可能存在两个点分别属于它们,并且由道路连通(否则就能回去了,违反题意)。换句话说,道路连通块里任意两点之间不可能有航线。并且,两个道路连通块之间只能有单向的航线,所以所有的道路连通块可以看成是一个有向无环图,可以用拓扑排序的顺序计算每个道路连通块的点的最短路长度。而道路的权值都是非负的,所以在道路连通块内部是可以用Dijkstra算法的。具体来说算法如下:
1、先将每个道路连通块找出来,具体来说,要对每个道路连通块进行编号,然后分别存一下每个顶点所在连通块的编号,以及每个编号的连通块有哪些顶点;
2、接着按照拓扑序来更新连通块里的点的最短路长度。这里需要用BFS来做,首先将所有入度为
0
0
0的连通块编号入队,此时这个连通块里的点的最短路长度都已经被其所有拓扑序上更前面的点更新过了(当然第一批入队的连通块没别人更新它,但是之后入队的连通块都有这个性质)。由于连通块内部的道路都是非负权的,所以可以用Dijkstra算法来求出连通块里每个点的最短路长度。连通块里的点被更新的时候是要入堆的,但是连通块外部的点被更新的时候不能入堆,而要记录一下入度减
1
1
1。这里的”连通块外部的点“指的是拓扑序相较当前连通块更后的连通块里的点,这些点的最短路是有可能被负权边更新的,不满足Dijkstra算法的要求,所以不能入堆。入度减
1
1
1是为了做拓扑排序。当某个连通块的入度变为
0
0
0的时候,就可以断定这个连通块里的点的最短路距离都已经被其拓扑序更小的连通块里的点更新过了(当然它们内部还能互相更新,这就要用Dijkstra做一遍),所以要将该连通块编号入队;
3、重复上述操作,直到每个连通块都遍历完为止;
4、最后整理答案的时候,有些点虽然不可达,但是可能被负权边更新过,所以在判断不可达的时候,要写成大于略小于正无穷的数。
代码如下:
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 25010, M = 150010, INF = 0x3f3f3f3f;
int n, mr, mp, S;
int h[N], e[M], ne[M], w[M], idx;
// id[i]存的是点i所在的道路连通块的编号,编号从1开始
int id[N];
// block[i]是编号i的道路连通块里的所有点的编号
vector<int> block[N];
// dist[i]是从S到i的最短路长度,din[i]是点i的入度
int dist[N], din[N];
// bcnt是道路连通块的个数
int bcnt;
bool st[N];
queue<int> q;
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// bid是u所在的道路连通块的编号,从1开始计数。dfs函数将这个连通块的所有顶点标记为bid
void dfs(int u, int bid) {
id[u] = bid;
block[bid].push_back(u);
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (!id[j]) dfs(j, bid);
}
}
void dijkstra(int bid) {
priority_queue<PII, vector<PII>, greater<PII> > heap;
for (int ver : block[bid]) heap.push({dist[ver], ver});
while (heap.size()) {
PII t = heap.top();
heap.pop();
int ver = t.second, d = t.first;
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] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
// 只有在同一道路连通块里的点被更新了才能入堆
if (id[j] == id[ver]) heap.push({dist[j], j});
}
if (id[j] != id[ver] && --din[id[j]] == 0) q.push(id[j]);
}
}
}
void topsort() {
memset(dist, 0x3f, sizeof dist);
dist[S] = 0;
// 把入度为0的连通块编号入队
for (int i = 1; i <= bcnt; i++)
if (!din[i]) q.push(i);
// 按连通块之间的拓扑序,依次用Dijkstra算法计算每个连通块的点的最短路,
// 同时如必要,以之更新拓扑序较后的连通块的点的最短路
while (q.size()) {
int t = q.front();
q.pop();
dijkstra(t);
}
}
int main() {
scanf("%d%d%d%d", &n, &mr, &mp, &S);
memset(h, -1, sizeof h);
while (mr--) {
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])
dfs(i, ++bcnt);
while (mp--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
din[id[b]]++;
}
topsort();
for (int i = 1; i <= n; i++)
// 无穷大有可能被负的航线更新,所以这里判断无穷大的时候要大于一个”较小的“无穷大
if (dist[i] > INF / 2) cout << "NO PATH" << endl;
else cout << dist[i] << endl;
return 0;
}
时间复杂度 O ( m log n ) O(m\log n) O(mlogn), m m m是总边数, n n n是总点数,空间 O ( n ) O(n) O(n)。