1. 程式人生 > 實用技巧 >Roads and Planes

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

為了適應本題需要,上面程式碼並不是拓撲排序的模板

下面給出拓撲排序的虛擬碼模板:

  1. 資料結構:inder[i]頂點i的入度,stack[] 棧
  2. 初始化:top=0(棧頂指標置零)
  3. i=0(計數器)
  4. while(棧非空)
    • 棧頂頂點v出棧;top-1;輸出v;i++;
    • for v的每一個後繼頂點u
      1. inder[i]--; u的入度減1
      2. 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