題目描述:
農夫約翰正在一個新的銷售區域對他的牛奶銷售方案進行調查。
他想把牛奶送到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;
}