Roads and Planes
A 前言
(本文搬運自作者學校WordPress中 作者所寫文章)
對於2020.8.5日的C2022同學們
這題有yi點超綱,
所以,裡面會涉及到一些新的概念,新的知識
預計裡面的概念在2020.8.6日我們可以全面掌握
所以不再題解中過多對概念進行詮釋
同時,陳同學講解的SPFA的SLF優化是可以水過的,但並不是本題的正解
正解oj上用時200ms
而加了SLF的SPFA還是用了1500ms勉強卡過(時限2s)
B 題面
C 解題
我們把題目稍微概括一下:
有 T 個點,R 條雙向邊,P 條單向邊。其中雙向邊權值均為正,單向邊權值可以為負。
保證如果有一條航線可以從a到b,那麼保證不可能通過一些道路和航線從b回到a。
請你求出 S 到所有點的最短距離,若不能到達則輸出"NO PATH"
資料範圍: 1<=T<=25000 , 1<=R,P<=50000
容易發現,這是一個帶負權的最短路
乍一看,我們可以用SPFA(關於SPFA,它死了!!!),但顯然會被卡掉
而考慮dijkstra又會有負權的問題
所以我們需要從題目中挖掘一些特別的性質來解決這道題
我們把題目在讀一遍,看到這樣一句話:
保證如果有一條航線可以從a到b,那麼保證不可能通過一些道路和航線從b回到a
也就是說:
這個圖不存在負環,有可能存在正環
也就是說,如果只把雙向邊新增進來,那麼一定就形成了若干個強連通的塊。
或者是說,我們可以把雙向邊連線的點看作是一個整體
(不知道強聯通的看 )
圖片理解:(圖醜輕噴)
這其實運用到了縮點(瞭解更多)
而可能的負權邊又保證不成環(不反向聯通)
因此把無向邊聯通塊縮點後,得到的就是有向無環圖
所以,我們可以在塊內使用dijkstra最短路
塊間利用拓撲排序更新答案
(在這裡我假設我們知道拓撲排序 實在不知道看 這裡)
為什麼要用拓撲排序呢?
- 拓撲排序可以處理負邊權
- 拓撲排序時間複雜度是O(V+E)
- 拓撲排序得到的路徑顯然就是答案
(注:拓撲排序的缺點在於要求出發點入度為0,終點出度為0 且 不能處理無向邊)
C1 定義
由於本題變數實在太多,所以我把它們定義在一起,
方便大家看到後面突然對變數名迷茫了 可以回頭看一看
(自認為變數名含義還是比較明瞭的)
const int N=25003,M=150003,INF=0x3f3f3f3f; struct Edge{ int next; //下一條邊 int dis; //邊權 int to; //下一個點 }edge[M]; int t,r,p,s,elen; //t個點,r條雙向邊,p條單向邊,起點s,edge[]長度elen int head[N]; //head[i]是i的第一條邊 int id[N]; //id[i]是i點所在的聯通塊編號 int dis[N]; //dis[i]是i點到起點的最短路 int vis[N]; //i是否被訪問過 int in[N]; //in[i]是i點拓撲排序的入度 queue<int>q; //拓撲排序的佇列 vector<int>block[N];//block[i]存聯通塊i中的點 int cnt_block; //聯通塊的個數
dijkstra
(我在這裡是使用的STL,瞭解更多 )
(背不住dijkstra模板的可以勸退了)
- 將每一個點插入堆中
- 取出堆中的最小值,遍歷與那個點相連的點(優先佇列實現)
- 如果可以更新最短距離,就更新;並且如果它們處於同一個連通塊中,就將遍歷到的點插入堆中。
- 如果它們不在同一個聯通塊裡面,且遍歷到的點入度為0,則將這個點插入拓撲排序的佇列裡
我們一步步來:
1.定義一個優先佇列:
priority_queue <pair <int ,int > > qh;
同時需要說明的的是:其實優先佇列是實現堆實現的,預設是大根堆
所我們可以把它理解成堆的STL
第一個排序依據是pair的first
第二個排序依據是pair的second。
第一關鍵字是距離 dis[i]
第二關鍵字自然是 i
2.將每一個點插入堆中
int len=block[now].size(); //.size()獲得block[i]的長度 for(int i=0;i<len;i++){ qh.push(make_pair(-dis[block[now][i]],block[now][i]));//將每一個點插入佇列中 }
ph.push(a)表示把a插入佇列
make_pair(a,b)表示生成資料
值得注意的是,因為這預設是大根堆
而我們需要的是小根堆,所以我們可以通過加個符號的方法
讓他實際上是一個小根堆
(其實所謂“預設”是可以調整的,可以見袁同學的 15分鐘掌握STL)
3.取出堆中的最小值,遍歷與那個點相連的點
while(!qh.empty()){ //堆不空 int u=qh.top().second; //其實取出的是最小值的位置 qh.pop(); //取出後要把堆頂刪除(彈出) if(vis[u])continue; //跳過 vis[u]=1; //標記 for(int e=head[u];e;e=edge[e].next){//遍歷邊 //do the other things... } }
qh.second是pair中第二個值
4.如果可以更新最短距離,就更新;並且如果它們處於同一個連通塊中,就將遍歷到的點插入堆中
這裡其實是對for迴圈內語句的描述
(忘記變數名意思的可以回頭看一眼)
int v=edge[e].to; if(dis[v]>dis[u]+edge[e].dis){ //可以更新距離 dis[v]=dis[u]+edge[e].dis; if(id[v]==id[u]) //在同一個聯通塊中 qh.push(make_pair(-dis[v],v));//插入堆 }
注意預設是大根堆
5.如果它們不在同一個聯通塊裡面,且遍歷到的點入度為0,則將這個點插入拓撲排序的佇列裡
也就是:
if(id[v]!=id[u]&&!--in[id[v]])q.push(id[v]);
我們把上述程式碼串起來:
inline void dijkstra(int now){ priority_queue <pair <int ,int > > qh;//定義一個優先佇列 int len=block[now].size(); //.size()獲得block[i]的長度 for(int i=0;i<len;i++){ qh.push(make_pair(-dis[block[now][i]],block[now][i]));//將每一個點插入堆中 } while(!qh.empty()){ //堆不空 int u=qh.top().second; //其實取出的是最小值的位置 qh.pop(); //取出後要把堆頂刪除(彈出) if(vis[u])continue; //跳過 vis[u]=1; //標記 for(int e=head[u];e;e=edge[e].next){//遍歷邊 int v=edge[e].to; if(dis[v]>dis[u]+edge[e].dis){ //可以更新距離 dis[v]=dis[u]+edge[e].dis; if(id[v]==id[u]) //在同一個聯通塊中 qh.push(make_pair(-dis[v],v));//插入堆 } if(id[v]!=id[u]&&!--in[id[v]])q.push(id[v]); //下一個塊可以進行拓撲排序 } } }
其他
拓撲排序
inline void toposort(){ //拓撲排序 memset(dis,0x3f,sizeof(dis)); //初始化最短距離 dis[s]=0; for(int i=1;i<=cnt_block;i++){//cnt_block:塊的個數 if(!in[i])q.push(i); //先加入入度為0 的點 } while(!q.empty()){ //非空 int t=q.front();q.pop(); //取出第一個點並彈出 dijkstra(t); //對每個聯通塊做dijkstra } }
拓撲排序的定義:
如果a排在b前面,則b不能通過任何方式到達a
為了適應本題需要,上面程式碼並不是拓撲排序的模板
下面給出拓撲排序的虛擬碼模板:
- 資料結構:inder[i]頂點i的入度,stack[] 棧
- 初始化:top=0(棧頂指標置零)
- i=0(計數器)
- while(棧非空)
- 棧頂頂點v出棧;top-1;輸出v;i++;
- for v的每一個後繼頂點u
- inder[i]--; u的入度減1
- if indet[i]==0 頂點 u 入棧
可以據此幫助理解上面的內容
DFS求id[]
void dfs(int u,int nowid){ id[u]=nowid; block[nowid].push_back(u); for(int e=head[u];e;e=edge[e].next){ int v=edge[e].to; if(!id[v])dfs(v,nowid); } }
注意id[i]表示i點所在的聯通塊的編號
建立邊
這個就是常規操作了(鄰接表建邊)
inline void add(int from,int to,int dis){ edge[++elen].next=head[from]; edge[elen].to=to; edge[elen].dis=dis; head[from]=elen; }
main函式
在本題中,尤其需要注意上述函式的執行位置和順序,比如:
dfs函式必須在單項邊輸入前執行,因為聯通塊中不含單向邊
當然不只這一個細節
int main(){ scanf("%d%d%d%d",&t,&r,&p,&s); for(int i=1;i<=r;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } for(int i=1;i<=t;i++){//求每個點所在的聯通塊編號 if(!id[i]){ cnt_block++; dfs(i,cnt_block); } } for(int i=1;i<=p;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); in[id[v]]++;//統計入度 } toposort();//拓撲排序 for(int i=1;i<=t;i++){ if(dis[i]>INF/2)printf("NO PATH\n");//判無解 else printf("%d\n",dis[i]); } return 0; }
注意判誤解的要求是inf/2
這是為了判斷無法到達時還用負數去更新的情況
D 程式碼
#include<bits/stdc++.h> using namespace std; const int N=25003,M=150003,INF=0x3f3f3f3f; struct Edge{ int next; //下一條邊 int dis; //邊權 int to; //下一個點 }edge[M]; int t,r,p,s,elen; //t個點,r條雙向邊,p條單向邊,起點s,edge[]長度elen int head[N]; //head[i]是i的第一條邊 int id[N]; //id[i]是i點所在的聯通塊編號 int dis[N]; //dis[i]是i點到起點的最短路 int vis[N]; //i是否被訪問過 int in[N]; //in[i]是i點拓撲排序的入度 queue<int>q; //拓撲排序的佇列 vector<int>block[N];//block[i]存聯通塊i中的點 int cnt_block; //聯通塊的個數 inline void add(int from,int to,int dis){ edge[++elen].next=head[from]; edge[elen].to=to; edge[elen].dis=dis; head[from]=elen; } void dfs(int u,int nowid){ id[u]=nowid; block[nowid].push_back(u); for(int e=head[u];e;e=edge[e].next){ int v=edge[e].to; if(!id[v])dfs(v,nowid); } } inline void dijkstra(int now){ priority_queue <pair <int ,int > > qh;//定義一個優先佇列 int len=block[now].size(); //.size()獲得block[i]的長度 for(int i=0;i<len;i++){ qh.push(make_pair(-dis[block[now][i]],block[now][i]));//將每一個點插入堆中 } while(!qh.empty()){ //堆不空 int u=qh.top().second; //其實取出的是最小值的位置 qh.pop(); //取出後要把堆頂刪除(彈出) if(vis[u])continue; //跳過 vis[u]=1; //標記 for(int e=head[u];e;e=edge[e].next){//遍歷邊 int v=edge[e].to; if(dis[v]>dis[u]+edge[e].dis){ //可以更新距離 dis[v]=dis[u]+edge[e].dis; if(id[v]==id[u]) //在同一個聯通塊中 qh.push(make_pair(-dis[v],v));//插入堆 } if(id[v]!=id[u]&&!--in[id[v]])q.push(id[v]); //下一個塊可以進行拓撲排序 } } } inline void toposort(){ //拓撲排序 memset(dis,0x3f,sizeof(dis)); //初始化最短距離 dis[s]=0; for(int i=1;i<=cnt_block;i++){//cnt_block:塊的個數 if(!in[i])q.push(i); //先加入入度為0 的點 } while(!q.empty()){ //非空 int t=q.front();q.pop(); //取出第一個點並彈出 dijkstra(t); //對每個聯通塊做dijkstra } } int main(){ scanf("%d%d%d%d",&t,&r,&p,&s); for(int i=1;i<=r;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } for(int i=1;i<=t;i++){//求每個點所在的聯通塊編號 if(!id[i]){ cnt_block++; dfs(i,cnt_block); } } for(int i=1;i<=p;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); in[id[v]]++;//統計入度 } toposort();//拓撲排序 for(int i=1;i<=t;i++){ if(dis[i]>INF/2)printf("NO PATH\n");//判無解 else printf("%d\n",dis[i]); } return 0; }View Code