题目描述:
农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。
他想把牛奶送到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
分析:
本题解题逻辑比较复杂,但是一旦理顺了思路,也是可以很快AC的。首先分析下题意,城镇之间有两种路径,双向、边权非负的道路,以及单向、边权可能是负数的航线,并且航线不存在环。抽象成图模型就是有两类边,正权的双向边和可以是负权的单向边,若存在从a到b的单向边,则b不可能通过一些单向边或者双向边到达a。如果只当普通的含负权边的最短路问题,只需要用spfa算法就可以求解,但是本题测试数据会卡掉spfa,卡成O(nm)后,25000*150000显然会超时,因此需要采取更加高效的解法去求解。
首先讲下预备知识,我们知道,拓扑排序可以求一个DAG的拓扑序列,从而确定任务完成的先后关系,但是容易忽略的是拓扑排序也可以求DAG的最短路径长度,不管存不存在负权边。证明也很简单,考虑一般的数学归纳法即可证明,假设一个点的前驱节点离起点的最短距离都确定了,则这个节点离起点的最短距离可以通过前驱结点加上到该节点的边权的最小值决定。边界情况是起点的入度为0时,其后继节点的最短路就可以直接通过比较确定下来。虽然本题并没有用这种办法去求DAG的最短路径,但是节点最短路的求解顺序却是按照拓扑序来的。
直接说下解题思路,道路连成的顶点构成若干个连通块,我们将每个连通块看成一个大的节点,则原图就抽象为了由这些大节点构成的DAG了。连通块内都是正权边,可以用dijkstra求最短路,我们按照拓扑序的顺序依次对各个连通块内的节点作dijkstra,来求出连通块内部节点的最短路径,本题就解决了。(这里偷懒就不把样例的图画出来了)
如果只是简单地描述下思路很多人仍是云里雾里,但是只要走一遍代码执行的流程,算法的正确性便显而易见了。
首先,读入所有的道路信息(双向边),我们要统计出有多少个连通块,并且每个点属于哪个连通块,每个连通块有哪些节点,这就相当于flood fill问题,做一遍DFS即可解决。
void dfs(int u){
id[u] = bcnt,block[bcnt].push_back(u);
for(int i = h[u];~i;i = ne[i]){
int j = e[i];
if(!id[j]){
dfs(j);
}
}
}
id[u]表示u节点属于的连通块编号,其中id[j]为0表示还没有 j还没有加入本连通块,加入即可,DFS的过程很简单,不再赘述。
然后再读入航线信息,此时把航线的单向边都加上也不会影响连通块的统计了,这就是巧妙之处,同时统计各个连通块的入度信息。
最后做拓扑排序,完成后如果某个节点离起点的距离还是很大,就表明没有路径,因为含负权边,一般超过INF / 2就视为没有路径了。
算法的核心在于拓扑排序,遍历各个连通块,将入度为0的连通块编号加入到队列中,队列非空时取队头连通块,对该连通块做dijkstra求最短路。在对连通块内节点做dijkstra时,由于不知道哪个节点离起点最近,所以将所有的点都放入小根堆中,取堆顶元素即可。设堆顶元素为u,如果u已经出过优先级队列了,就continue,否则,对周边点执行松弛操作。这里的松弛操作与常规的松弛操作不同的是要判断周围的点是否与u在同一连通块内,如果不在,就将u指向的连通块入度减一,减到0的时候加入到拓扑排序的队列中去。不管周围的点与u是否在同一连通块内,都要去松弛这个点,但是只有同一个连通块内的点被松弛了才需要加入到堆中。因此,对连通块做dijkstra的效果是这个连通块内部点的最短距离都求出来了,同时也松弛了相邻连通块的点的距离,更新了周围连通块的入度信息。
算法到这里就结束了,两点需要注意,其一是虽然dijkstra算法一开始是将连通块内所有点都加入堆,类似于求多源最短路,但是却不是求多源最短路,只是确定下最小的距离而已;其二是在做拓扑排序的过程中,只有起点S所在的连通块做完dijkstra后其他连通块的距离才会被更新,或者说更新才有意义,那么能否在拓扑排序时直接忽略S所在连通块出队前的出队连通块编号呢?答案是否定的。因为dijkstra的作用不仅是更新连通块内点的最短路和相邻连通块点的距离,还要更新相邻连通块的入度信息,在S所在的连通块出队前不用求最短路,但是却要更新周围连通块的入度信息,dijkstra顺带做了这件事,所以还是有必要的。
总的代码如下:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
const int N = 25005,M = 150005,INF = 0x3f3f3f3f;
typedef pair<int,int> PII;
int idx,h[N],e[M],w[M],ne[M];
int T,R,P,S,bcnt,d[N],id[N],inc[N];
vector<int> block[N];
bool st[N];
queue<int> q;
priority_queue<PII,vector<PII>,greater<PII> > pq;
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){
id[u] = bcnt,block[bcnt].push_back(u);
for(int i = h[u];~i;i = ne[i]){
int j = e[i];
if(!id[j]){
dfs(j);
}
}
}
void dijkstra(int t){
for(auto x : block[t]) pq.push({d[x],x});
while(pq.size()){
int u = pq.top().second;
pq.pop();
if(st[u]) continue;
st[u] = true;
for(int i = h[u];~i;i = ne[i]){
int j = e[i];
if(st[j]) continue;
if(id[u] != id[j] && --inc[id[j]] == 0) q.push(id[j]);
if(d[j] > d[u] + w[i]){
d[j] = d[u] + w[i];
if(id[u] == id[j]) pq.push({d[j],j});
}
}
}
}
void topsort(){
for(int i = 1;i <= bcnt;i++){
if(!inc[i]) q.push(i);
}
while(q.size()){
int u = q.front();
q.pop();
dijkstra(u);
}
}
int main(){
memset(h,-1,sizeof h);
memset(d,0x3f,sizeof d);
scanf("%d%d%d%d",&T,&R,&P,&S);
d[S] = 0;
int a,b,c;
while(R--){
scanf("%d%d%d",&a,&b,&c);
add(a,b,c),add(b,a,c);
}
for(int i = 1;i <= T;i++){
if(!id[i]){
bcnt++;
dfs(i);
}
}
while(P--){
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
inc[id[b]]++;
}
topsort();
for(int i = 1;i <= T;i++){
if(d[i] > INF / 2) printf("NO PATH\n");
else printf("%d\n",d[i]);
}
return 0;
}