基礎圖論

恩對我們要來講圖論了

怎麼自學?

聽不懂?那就自己讀讀看書

(恩對自學很重要)

Antti Laaksonen - Competitive Programmer’s Handbook

圖論在第107頁

這個簡報有些東西也是從這本書拿來的

怎麼自學?

聽不懂?那就自己查資料

(恩對自學很重要)

例如:

自己在 CSES 刷題

自己在 cp-algorithms 查資訊

講師介紹:

成電CKCSC 38th - 教學 + 網管

翁釩予

MLGnotCOOL

@mlgnotcool

 

自我介紹網站?

講師介紹:

興趣?(除了扣丁之外)

講師介紹:

學術力:

競程、演算法、網頁設計、python

APCS 高級 5+5、北市賽三等獎 第17名

(bruh TOI 初選炸掉)菜

歐對 我打字很快算學術力嗎 :U(NO)

圖?

圖是什麼 可以吃嗎?

圖論

圖? (picture)

圖? (graph)

圖論

圖(Graph)

為離散數學的一部分 (恩我們要開始講數學了)

 

圖用來表示有限集合中物件之間的關係,物件在圖中以「節點/頂點(node / vertex)」呈現、關係以「邊(edge / arc)」呈現。

圖論 (名詞)

點 (node) :在圖上的節點 (可以想像成公車站)

邊 (edge) :在圖上連接點的邊 (可以想像成公車路線)

圖論 (名詞)

有向圖 (directed graph):由有向邊形成的圖 (可以想像成單成票)

無向圖 (undirected graph):由無向邊形成的圖 (可以想像成來回票)

* 無向圖也可以稱為簡單圖

有向圖:

無向圖:

圖論 (名詞)

有權圖 (weighted graph):邊上有附於值 (可以想像成公車票價)

無權圖 (unweighted graph):邊上沒有值

圖論 (名詞)

連通塊:

每個點可以互相走到就叫做一個連通塊

(可以想像成一個城市的捷運和另外一個城市的捷運,分成兩塊)

連通塊 1

連通塊 2

圖論 (名詞)

度數(degree):

一個點他有幾條邊連接 (可以想像成這站有幾台公車)

有向圖時,可以分成in度數和out度數

點C的度數為2

點C的in度數為2,out度數為1

圖論 (名詞)

樹(tree):一個簡單無環圖,由n-1條邊組成的圖

圖論 (名詞)

根(root),父節點 (parent node),

根節點 (child node),子樹(subtree)

根 (為點 2 5 9的父節點)

為點1個根節點

為點5的子樹

圖論 (名詞)

較不常用到但是會用到的名詞:

 

循環 (cycle):

每個點度數為2,形成的一個循環的圖 (或是in=out度數=1)

 

鍊 (chain):

頭尾的度數為1,其他點度數為2,邊數是n-1,形成一條鍊

存圖

存圖

假設我們這裡有一張圖,那我們該如何處存它

最直接的作法就是存一個二維陣列,可以互相走到就設為1 (ex: a[1][2] = 1,1可以走到2)

這也叫做鄰接陣矩 (adjacency matrix)

存圖

但是鄰接矩陣會發現一個問題,就是在很稀疏的圖它會浪費很多空間,並且會需到 O(n^2) 的空間,空間幾乎是白開的

bool f[maxn][maxn];
for (int i=1; i<=n; ++i){
    for (int j=1; j<=n; ++j) cin >> f[i][j];
}

存圖

如果圖很稀疏,我們只要存每個點可以走到點就好了

作法就是每個點開一個vector,並且放入那個點可以走到的點

這也叫做鄰接陣列 (adjacency list)

存圖

這樣的空間只有 O(m),可以省下很多空間

(m 為圖中的邊數)

vector<int> e[maxn];
for (int i=1; i<=m; ++i){
    int u, v; 
    cin >> u >> v;
    e[u].push_back(v);
    e[v].push_back(u);
}

u, v 代表兩點之間有無向邊

那我們就兩點都要存

(相反的 有向邊就只要存開頭的那個點)

BFS

廣度優先搜尋

BFS

假設我們有一張無權圖,要從A點開始,遍歷整張圖

我們可以一直慢慢擴散出去,不斷尋找下點可以去哪

BFS

從A點出發,可以走到 B C

BFS

從B C點出發,可以走到 D E F

注意:走過的就不用在走了

BFS

從D E F點出發,可以走到 H I

BFS

從H I點出發,可以走到J

最後我們就成功用BFS遍歷整張圖了

時間複雜度為 O(n+m)

BFS

並且會發現一個特性

因為我們是一層一層的作,所以我們抵達一個點時他一定是最短距離,所以BFS可以用來求無權圖點到其他點的最短距離

BFS

實作:

我們可以使用queue,將每次可以走到的點推進queue裡

因為queue是先進先出,所以我們先到的會先處理,比較晚到的會後面處理,就達到一層一層處理的效果了

 

我們同時紀錄一個陣列,檢查有沒有走過,如果有就不要在遍歷那個點

重複直到queue是空的

BFS

ZeroJudge a290 新手訓練系列 ~ 圖論https://zerojudge.tw/ShowProblem?problemid=a290

給點與點之間的有向邊

求可不可以從一點走到一點

題目1:

BFS

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 805
using namespace std;

int n, m, s, t;
vector<int> e[maxn];
bool vis[maxn];
bool f;
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    while (cin >> n >> m){
        //__init__
        f = false; queue<int> q;
        for (int i=1; i<=n; ++i){
            vis[i] = 0;
            e[i].clear();
        }

        for (int i=1; i<=m; ++i){
            int u, v; cin >> u >> v;
            e[u].push_back(v);
        }
        cin >> s >> t;

        q.push(s);
        while (!q.empty()){
            int u = q.front(); q.pop();

            vis[u] = 1;
            for (int v:e[u]){
                if (vis[v]) continue;

                q.push(v);
            }
        }

        if (vis[t]) cout << "Yes!!!" << endl;
        else cout << "No!!!" << endl;
    }
}

輸入圖 存圖

BFS

判斷最後有沒有走到

ZeroJudge a290 新手訓練系列 ~ 圖論

BFS

給予一個二維圖,求一點到另一點最短路徑,且找出隨便一條路徑。

題目2:

BFS

CSES - Labyrinth

這題其實可以轉成我們剛剛的圖,就是每個點它可以走到點就是它鄰近的四個點,且這幾個點沒有超過範圍,且不是牆

if (nxtx < 1 || nxtx > n || nxty < 1 || nxty > m) continue;
if (vis[nxtx][nxty]) continue;
if (c[nxtx][nxty] == '#') continue;

BFS

CSES - Labyrinth

我們求路徑的方法其實很簡單,我們只要在走到新的點時,紀錄我們從哪個位置走過來就好了

然後最後在一直走回去就好了

q.push({nxtx, nxty, d+1});
vis[nxtx][nxty] = 1;
//等一下解釋k是什麼,先別及
if (k == 0) dir[nxtx][nxty] = 'R';
else if (k == 1) dir[nxtx][nxty] = 'L';
else if (k == 2) dir[nxtx][nxty] = 'D';
else if (k == 3) dir[nxtx][nxty] = 'U';
int x = e.first, y = e.second;
 
while (x != s.first || y != s.second){
    ans.push_back(dir[x][y]);
    if (dir[x][y] == 'R') --y;
    else if (dir[x][y] == 'L') ++y;
    else if (dir[x][y] == 'U') ++x;
    else if (dir[x][y] == 'D') --x;
}
 
for (int i=ans.size()-1; i>=0; --i) cout << ans[i];

BFS

CSES - Labyrinth

如果題目是二維圖,我們有一個小技巧,就是紀錄每個方向它改變的x, y

 

然後遍歷陣列,加上我們的現在的點,就會是可以走到的點了

int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};

for (int k=0; k<4; ++k){
    int nxtx = x + dx[k], nxty = y + dy[k];
}

BFS

CSES - Labyrinth

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 1005
#define int long long
using namespace std;
 
struct coord{
    int x, y, d;
};
 
int n, m;
pair<int, int> s, e;
char c[maxn][maxn], dir[maxn][maxn];
queue<coord> q;
bool f, vis[maxn][maxn];
int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
vector<char> ans;
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=n; ++i){
        for (int j=1; j<=m; ++j){
            cin >> c[i][j];
 
            if (c[i][j] == 'A') s = {i, j};
            if (c[i][j] == 'B') e = {i, j};
        }
    }
 
    q.push({s.first, s.second, 0});
    vis[s.first][s.second] = 1;
    while (!q.empty()){
        int x = q.front().x, y = q.front().y, d = q.front().d; q.pop();
 
        if (x == e.first && y == e.second){
            f = true;
            cout << "YES" << endl;
            cout << d << endl;
            break;
        }
 
        for (int k=0; k<4; ++k){
            int nxtx = x + dx[k], nxty = y + dy[k];
 
            if (nxtx < 1 || nxtx > n || nxty < 1 || nxty > m) continue;
            if (vis[nxtx][nxty]) continue;
            if (c[nxtx][nxty] == '#') continue;
 
            q.push({nxtx, nxty, d+1});
            vis[nxtx][nxty] = 1;
            if (k == 0) dir[nxtx][nxty] = 'R';
            else if (k == 1) dir[nxtx][nxty] = 'L';
            else if (k == 2) dir[nxtx][nxty] = 'D';
            else if (k == 3) dir[nxtx][nxty] = 'U';
        }
    }
 
    if (f){
        int x = e.first, y = e.second;
 
        while (x != s.first || y != s.second){
            ans.push_back(dir[x][y]);
            if (dir[x][y] == 'R') --y;
            else if (dir[x][y] == 'L') ++y;
            else if (dir[x][y] == 'U') ++x;
            else if (dir[x][y] == 'D') --x;
        }
 
        for (int i=ans.size()-1; i>=0; --i) cout << ans[i];
    }else cout << "NO" << endl;
}

BFS

一些題目:(要答案來找我)

簡單:CSES - Message Route

 

中等:ZJ c124

 

困難:

CSES - Monsters

(提示:先多源BFS檢查殭屍可以走到點最快的時間,然後人走的時候檢查就好了)

abc 420 pD - Toggle Maze

(提示:他開和關可以視為兩個不同的點,這兩個點可以之間移動,且保證我們只會走過個點一次,因為走回去會浪費時間)

DFS

深度優先搜尋

假設我們有一張無權圖,要從A點開始,遍歷整張圖

我們可以像走迷宮,不斷往下走直到死路,然後往回走

DFS

點1先走到點2

DFS

點2走到點3

點3走到點5

因為都是死路,所以一路反回到1

DFS

回到點1後,我們找下個點,就會是點4

然後接下來是死路,所以就結束了

 

最後我們就成功用DFS遍歷整張圖了

時間複雜度為 O(n+m)

DFS

並且會發現一個特性

和BFS不同的是,它沒辦法找最短路徑,但是非常好寫

(等一下會解釋)

DFS

實作:

我們可以用遞迴,一直把第一個點遞迴下去,這樣就可以達到不斷往下走的效果了

void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;
        dfs(i);
    }
}

可以看到dfs的扣很短,比bfs好寫

DFS

ZeroJudge a290 新手訓練系列 ~ 圖論https://zerojudge.tw/ShowProblem?problemid=a290

給點與點之間的有向邊

求可不可以從一點走到一點

題目1:

DFS

ZeroJudge a290 新手訓練系列 ~ 圖論

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 805
using namespace std;

int n, m, s, t;
vector<int> e[maxn];
bool vis[maxn];
bool f;

void dfs(int x){
    vis[x] = 1;
    for (int i:e[x]){
        if (vis[i]) continue;
        dfs(i);
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    while (cin >> n >> m){
        //__init__
        f = false; queue<int> q;
        for (int i=1; i<=n; ++i){
            vis[i] = 0;
            e[i].clear();
        }

        for (int i=1; i<=m; ++i){
            int u, v; cin >> u >> v;
            e[u].push_back(v);
        }
        cin >> s >> t;
        dfs(s);

        if (vis[t]) cout << "Yes!!!" << endl;
        else cout << "No!!!" << endl;
    }
}

輸入圖 存圖

DFS

判斷最後有沒有走到

DFS

題目2:

給予一個二維圖,求有幾個連通塊。

DFS

CSES - Counting Rooms

我們只要每次遇到沒走過的點,將它dfs,可以走到的點全部標記,它就會是一個新的房間了

剩下的技巧和上次教的一樣 (忘記可以往前翻複習一下)

DFS

CSES - Counting Rooms

DFS

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 1005
#define int long long
using namespace std;
 
int n, m, cnt; bool maze[maxn][maxn];
int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
 
void dfs(int x, int y){
    for (int i=0; i<4; ++i){
        if (maze[x+dx[i]][y+dy[i]] == 0) continue;
        
        maze[x+dx[i]][y+dy[i]] = 0;
        dfs(x + dx[i], y + dy[i]);
    }
}
 
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=n; ++i){
        for (int j=1; j<=m; ++j){
            char tmp; cin >> tmp;
            if (tmp == '#') maze[i][j] = 0;
            else maze[i][j] = 1;
        }
    }
 
    for (int i=1; i<=n; ++i){
        for (int j=1; j<=m; ++j){
            if (maze[i][j] == 0) continue;
            
            ++cnt;
            dfs(i, j);
        }
    }
 
    cout << cnt << endl;
}

DFS

一些題目:(要答案來找我)

DFS

一些題目:(要答案來找我)

困難:

abc 448 pD - Interger-duplicated Path 

(提示:樹)


abc 435 pD - Reachability Query 2 

(提示:黑點數只會增加,不會減少)


很困難:

Educational Codeforces Round 188 - pD Alternating Path

(提示:想想看,我們可以將題目轉移成什麼問題 上面有歐)

DSU

並查集

先來看題目:

DSU 並查集

我們要做到兩個動作:

1 - 將兩個集合並在一起

2 - 查詢兩個值是否在同個集合內

我們要用的就會是並查集!

DSU 並查集

首先我們將每個點都往外連一條有向邊到其他點,並且把他預設成自己

並且將定義成 最後走到的點的邊是指向自己的

DSU 並查集

我們要連的時候,只要把我們那個點的頭 指向另外一個點的頭就好了

舉例來說,我們要把1 2合併

就把2的頭 (2) 的邊指向1的頭 (1)

DSU 並查集

舉例來說,我們要把2 3合併

就把2的頭 (1) 的邊指向3的頭 (3)

DSU 並查集

* 我們找頭的方式 只要不斷往下走

直到走到邊指向自己的就代表找到頭了

(ex: 2 -> 1 -> 3 -> 3,3就會是頭)

DSU 並查集

* 我們只要確認兩個點是否在同個集合,

只要檢查頭是否相同就好了

(ex: 1和2的頭都是3,故在同個集合內)

DSU 並查集

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
using namespace std;

int n, t, parent[maxn];
string i1; int i2, i3;

inline void build(){
    for (int i=1; i<=n; ++i) parent[i] = i;
}

int find_set(int v) {
    if (v == parent[v]) return v;
    return find_set(parent[v]);
}

void union_set(int a, int b) {
    a = find_set(a);
    b = find_set(b);
    if (a != b) {
        parent[b] = a;
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> t;
    build();
    while (t--){
        cin >> i1 >> i2 >> i3;
        
        if (i1 == "union"){
            union_set(i2, i3);
        }else{
            if (find_set(i2) == find_set(i3)) cout << "YES" << endl;
            else cout << "NO" << endl ;
        }
    }
}

DSU 並查集

但是,這樣會發現一個問題:

 

就是我們會非常的慢,因為每次找頭都有可能花到 O(n)

 

但是我們可以作兩個優化來幫助我們壓低時間

啟發式合併

我們可以做到一個很簡單的優化:

每次都只把小的集合併入大的集合

啟發式合併

那這樣能省多少時間呢?

每次查詢都變成 O(log n)

啟發式合併

為什麼可以這樣呢?

每次將小集合併入大集合

設 sa 為大集合大小 , sb為小集合大小

有一個性質為 :

 

sza+szb>=szb∗2sa+sb>=sb2

啟發式合併

為什麼可以這樣呢?

由於上述性質,我們先把dsu想成一棵樹,

我們每往上走一步,他的子樹的點一定會多

2倍以上,故他最多只會走到O(log n)步

路徑壓縮

我們又可以做到一個很簡單的優化:

因為我們只在乎他的頭在哪裡,

所以我們在往上找得時候,只要把每個點都指向頭就好了

(就有點像我們把路徑都縮短的概念)

路徑壓縮

那這樣能省多少時間呢?

每次查詢都變成 O(log n)

DSU 並查集

那我們把兩個合併能省多少時間呢?

DSU + 啟發式合併 + 路徑壓縮 ->  O(α(n))

 

阿克曼函數

簡單來說,數字非常非常小,可以直接當作是 O(1)

DSU 並查集

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
using namespace std;

int n, t, parent[maxn], siz[maxn];
string i1; int i2, i3;

inline void build(){
    for (int i=1; i<=n; ++i){
		parent[i] = i;
        siz[i] = 1;
    }
}

int find_set(int v) {
    if (v == parent[v]) return v;
    return parent[v] = find_set(parent[v]); //壓路徑
}

void union_set(int a, int b) {
    a = find_set(a);
    b = find_set(b);
    if (a != b) {
        if (siz[a] < siz[b]) swap(a, b);
        //make sure that siz[b] < siz[a]
        //so b is the smaller set

        parent[b] = a;
        siz[a] += siz[b];
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> t;
    build();
    while (t--){
        cin >> i1 >> i2 >> i3;
        
        if (i1 == "union"){
            union_set(i2, i3);
        }else{
            if (find_set(i2) == find_set(i3)) cout << "YES" << endl;
            else cout << "NO" << endl ;
        }
    }
}

DSU 並查集

一些題目:(要答案來找我)

Dijkstra

然後要求一點到其他點的最短距離

(且邊權一定是正整數)

假設我們有一個有權圖

Dijkstra

Dijkstra

BFS

Dijkstra

Dijkstra

Dijkstra's 演算法的限制:

  • 權值(邊上面的值) 不能 為負數 (否則就要用Bellmen-Ford algorithm)
  • 只能求一點到其他點的最短距離(若要求隨機兩點的最短距離,就要用Floyd-Warshall algorithm)

Dijkstra

1. 將1到n的距離存到dist[n],並將dist全部設成無限 (dist[1]=0)

Dijkstra

2. A連接B和C,將B,C更新:

可將dist[2]更新為4,dist[3]更新為8 (4<inf, 8<inf)

Dijkstra

3. B距離最小,將B相鄰的點都更新
(因為C的8小於4+11,所以不用更新,也不用再加到pq)

並且若我們走過的點,則不用更新

Dijkstra

4. C距離最小,所以對C做一樣的事

(A, B走過,不用更新)

Dijkstra

5. F距離最小

Dijkstra

6. I距離最小

Dijkstra

7. D距離最小

Dijkstra

8. E距離最小

Dijkstra

9. H距離最小

Dijkstra

10. J距離最小

Dijkstra

為什麼可以這樣做呢?

當我們確定一個點的距離為最小時(就是他要往外更新的時候),就能確定他是最短距離

 

假設有一個更短的距離,那就代表圖上一定

有一個目前距離比他小的

因為距離比較小,就會先被處理掉

所以我們可以推論出不會有距離比他小

假設矛盾因此目前一定是最短距離

Dijkstra

由上面可以得知:

  • 目標:從距離最小的點不斷更新他附近(且沒走過)的點,並且將可更新的點 的新的距離存到dist[i]
  • dist[i]最小的點可以用一個將 距離由小到大排列 的priority_queue來查詢,且更新dist[i]時也要把點推到pq

 

  • pq裡有可能有重複的點,則如果此點已經經過的話,則直接跳過(因為比較小的距離之前就處裡過了)
  • 若pq是空的就停掉

時間複雜度由實作的方式而改變,這裡用的是最常用的方法,是 O((V + E) log V)

Dijkstra

給你點與點之間的距離(有方向性)

並且要求1到全部點(1~n)的最短距離

 

保證距離>=1,1都有可能到其他點

來看題目

Dijkstra

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 100005
#define inf 1e18
#define pii pair<int, int>
using namespace std;

int n, m;
vector<pii> e[maxn];
int dist[maxn];
bool vis[maxn];
priority_queue<pii, vector<pii>, greater<pii>> pq;
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v, c; cin >> u >> v >> c;
        e[u].push_back({v, c});
    }

    for (int i=1; i<=n; ++i) dist[i] = inf;
    dist[1] = 0;
    pq.push({0, 1});
    while (!pq.empty()){
        int u = pq.top().second; pq.pop();

        if (vis[u]) continue;
        vis[u] = 1;
        for (pii i:e[u]){
            int v = i.first, c = i.second;
            if (vis[v]) continue;

            if (dist[u] + c < dist[v]){
                dist[v] = dist[u] + c;
                pq.push({dist[v], v});
            }
        }
    }

    for (int i=1; i<=n; ++i){
        if (i == n) cout << dist[i] << endl;
        else cout << dist[i] << " ";
    }
}

Dijkstra

一些題目:(要答案來找我)

Bellmen-Ford

Bellmen-Ford

然後要求一點到其他點的最短距離

(並且會有負的邊權)

假設我們有一個有權圖

Bellmen-Ford 演算法的限制:

  • 權值(邊上面的值) 可以 為負數(可以修好Dijkstra的其中一個限制)
  • 他的複雜度是 O(n*m) 較大,比較不常在題目遇到

Bellmen-Ford

Bellmen-Ford

我們先假設一個東西:

我們從開頭走到點的最短路徑最多只會走過n-1個點

(因爲重複走一個點的話就會浪費)

 

這樣會有例外,等一下會解釋

Bellmen-Ford

那我們只要for回圈 n-1 次,每次將每個點他

鄰近的點都更新,就可以找到最短路徑了

 

(for回圈代表每走一步,並且看看

這步走到這點是否比較短)

Bellmen-Ford

來個圖走走看好了 我們一樣先把每個點距離設成無限,把起點設成0

Bellmen-Ford

我們走第一步,發現點 2 3 4 可以被更新

Bellmen-Ford

我們走第二步,發現點 4 5 可以被更新

Bellmen-Ford

我們走第三步,發現點 5 可以被更新

Bellmen-Ford

我們走第四步,發現點每個點都不能被更新

Bellmen-Ford

走完n-1步之後,就可以確保每個點都是最短距離了

而這樣的時間複雜度就會是 O(n*m)

Bellmen-Ford

但 我們會發現一個問題:

就是如果我們有負環怎麼辦?

(舉例來說 2 4 3 2 爲一個負環,我們不斷繞的話,

我們的距離會無限的變小)

Bellmen-Ford

其實非常好解:

因爲我們剛剛保證最短距離一定在n-1步可以達到

唯一的例外就是負環

 

我們只要檢查如果第n步還有更新的話,就帶表有負環

Bellmen-Ford

//e 存每條邊的資訊
for (int i=1; i<=n; ++i){
    bool flag = false;
    for (int j=1; j<=m; ++j){
        int u = e[i].u, v = e[i].v, d = e[i].d;

        if (dist[u] == inf) continue;
        if (dist[u] + d < dist[v]){
            flag = true;
            dist[v] = dist[u] + d;
        }
    }

    if (!flag) break;
    if (i == n && flag) f = true;
}
//f 會代表有沒有負環

Bellmen-Ford

給你點與點之間的分數(有方向性)

並且要求1走到n的最大分數

 

分數可以是負的

來看題目

Bellmen-Ford

因爲我們要找最大的,所以只要把羅輯改成找最大的就好了,剩下羅輯相同

並且在檢查負環時,如果走的到n才會影響解果,所以dfs檢查就好了

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
#define pii pair<int, int>
#define inf 1e18
using namespace std;

struct edge{
    int u, v, d;
};

int n, m, dist[maxn];
edge e[maxn];
vector<int> w[maxn];
bool f;
bool vis[maxn];

void dfs(int x){
    vis[x] = 1;
    for (int i:w[x]){
        if (vis[i]) continue;
        dfs(i);
    }
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        cin >> e[i].u >> e[i].v >> e[i].d;
        w[e[i].u].push_back(e[i].v);
    }

    for (int i=1; i<=n; ++i) dist[i] = -inf;
    dist[1] = 0;
    
    for (int i=1; i<=n; ++i){
        bool flag = false;
        for (int j=1; j<=m; ++j){
            int u = e[j].u, v = e[j].v, d = e[j].d;

            if (dist[u] == -inf) continue;
            if (dist[u] + d > dist[v]){
                flag = true;
                dist[v] = dist[u] + d;
                if (i==n) dfs(v);
            }
        }

        if (!flag) break;
        if (i == n && flag) f = true;
    }

    if (f && vis[n]) cout << -1 << endl;
    else cout << dist[n] << endl;
}

Bellmen-Ford

一些題目:(要答案來找我)

Floyd-Warshall

Floyd-Warshall

然後要求每個點到其他全部點的最短距離

假設我們有一個有權圖

Floyd-Warshall

Bellmen-Ford 演算法的限制:

  • 它可以找到每個點到其他所以點的最短距離 (可以修好Dijkstra的其中一個限制)
  • 他的複雜度是 O(n^3) 較大,比較不常在題目遇到

Floyd-Warshall

他的概念就是dp

 

大概的想法是,每次多考慮一個點,把每個走的方法都找到

dis_{k, i, j}

我們選定兩點 i, j

定義                  為從點 i 到點 j,

目前走了k步,他的最短距離

Floyd-Warshall

dis_{k,i,j} = min(dis_{k−1,i,j}, dis_{k−1,i,k}+dis_{k−1,k,j})

我們可以列出一個轉移式 :

概念有點像是,

我們試圖在 i, j 的路徑中加入k,看看會不會距離更小

Floyd-Warshall

最後的扣會長這樣:

//dis[0] 預設爲距離矩陣 (就是鄰接陣矩存距離)

for(int k=1;k<=n;++k){
	for(int i=1;i<=n;++i){
    	for(int j=1;j<=n;++j){
        	dis[k][i][j] = 
            min(dis[k-1][i][j], dis[k-1][i][k]+dis[k-1][k][j]);
        }
    }
}

Floyd-Warshall

然後可以發現空間上可以壓掉一層dp狀態 :

//dis[0] 預設爲距離矩陣 (就是鄰接陣矩存距離)

for(int k=1;k<=n;++k){
	for(int i=1;i<=n;++i){
    	for(int j=1;j<=n;++j){
        	dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
        }
    }
}

最後的表格就是 i到j 的最短距離了!

時間複雜度爲 O(n^3)

Floyd-Warshall

給你點與點之間的距離(有方向性)

並且要query 求 i到j 的最短距離

來看題目

Floyd-Warshall

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 505
#define inf 1e18
#define int long long
using namespace std;
 
int n, m, q, dist[maxn][maxn];
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m >> q;
 
    for (int i=1; i<=n; ++i){
        for (int j=1; j<=n; ++j){
            if (i!=j) dist[i][j] = inf;
        }
    }
 
    for (int i=1; i<=m; ++i){
        int u, v, w; cin >> u >> v >> w;
        dist[u][v] = min(dist[u][v], w);
        dist[v][u] = min(dist[v][u], w);
    }
 
    for (int k=1; k<=n; ++k){
        for (int i=1; i<=n; ++i){
            for (int j=1; j<=n; ++j){
                dist[i][j] = min(dist[i][j], dist[i][k]+dist[k][j]);
            }
        }
    }
 
    while (q--){
        int a, b; cin >> a >> b;
        if (dist[a][b] != inf) cout << dist[a][b] << endl;
        else cout << -1 << endl;
    }
}

Floyd-Warshall

一些題目:(要答案來找我)

MST

最小生成樹

生成樹 ?

生成樹是對於一個連通的無向圖

n 點 m 邊

 

有 n-1個邊

並且不存在環

MST

MST

簡單講就是保留一些邊

讓剩下來的邊形成一棵 n 點的樹

MST

最小生成樹 ?

 

在所有的生成樹中,所選邊權總和最小的

就是最小生成樹 MST

最小生成樹不一定唯一

 

所有邊權皆不同 -> 唯一

有邊權相同 -> 可能唯一

 

有可能最後最小的總和會相同

MST

舉例來說 底下的深黑為這個圖的最小生成樹

MST

最小生成樹有幾個性質 :

 

1 -  一個環上的邊權最大值,必屬於MST

2 - 一個cut上的最小邊權必屬於MST

cut 簡單來說,就是把一個連通塊選

一些邊切掉,把他分成兩個連通塊

MST

我們找到最小生成樹主要有兩種作法:

 

1 - Kruskal

2 - Prim

MST

我們用剛剛學到的一個性質:

一個環上的邊權最大值,必屬於MST

我們只要把邊由小到大加到圖裡面

如果遇到環的時候,因為樹裡面不會有環,

並且因為我們是由小到大加到圖裡,

 

所以一個環中最大的那個點不會被加到MST裡面

Kruskal

我們加入圖的時候會遇到兩種情況:

 

1 - 不為環

就直接加到圖裡就好了

 

2 - 為環

有上述得知,這個邊不要加到圖裡

Kruskal

那我們要怎麼判斷是否為環呢?

用並查及!

如果已經在同個集合內,那就代表是一個環了!

Kruskal

原圖:

先把原圖的邊先排序:

Kruskal

現從沒有邊的圖開始:

把邊由小到大加入圖:

Kruskal

不斷的加入圖中:

我們在遇到2-3的邊時,因為他們已經在同個集合內了,所以不加入,剩下的以此類推:

Kruskal

時間複雜度:

O(E log E + Eα(V))

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define maxn 200005
using namespace std;

struct edge{
    int u, v, w;
};

int n, m, parent[maxn], siz[maxn], cnt;
vector<edge> e; bool f=true;

bool comp(edge a, edge b){
    return a.w < b.w;
}

inline void build(){
    for (int i=1; i<=n; ++i){
        parent[i] = i;
        siz[i] = 1;
    }
}

int find_set(int v){
    if (parent[v] == v) return v;
    return parent[v] = find_set(parent[v]);
}

void union_set(int a, int b){
    if (siz[a] < siz[b]) swap(a, b);

    parent[b] = a;
    siz[a] += siz[b];
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v, d; cin >> u >> v >> d;
        e.push_back({u, v, d});
    }

    build();
    sort(e.begin(), e.end(), comp);

    for (int i=0; i<m; ++i){
        int v = e[i].v, u = e[i].u, w = e[i].w;
        int a = find_set(v), b = find_set(u);

        if (a != b){
            cnt += w;
            union_set(a, b);
        }
    }

    cout << cnt << endl;
}

Kruskal

Prim

我們試試看用另一種方式生成這個MST

我們從一個點出發,慢慢的將每個點加入這個MST

我們每次找到目前所有MST的點,往外連的邊(連到非MST)中

邊權最小的那個,並將那條邊以及連到的點加入MST

Prim

為什麼可以這樣?

由剛剛學到的性質二得知:一個cut上的最小邊權必屬於MST

舉例來說:假設我們目前的圖長這樣

那我們接下來可以選擇的邊有這些

這些可以選的點都是在cut上,因此我們要選最小的那個:

Prim

時間複雜度:

O(E +V log V)

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define pii pair<int, int>
#define maxn 100005

int n, m, sum; vector<pii> e[maxn];
bool vis[maxn];
priority_queue<pii, vector<pii>, greater<pii>> pq;
//pq.first -> the edge of the nodes in the MST, pq.second -> the node
//v[a].first -> node beside a, v[a].second -> dis between a and first

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);

    cin >> n >> m;
    for(int i=1;i<=m;++i){
        int u, v, d; cin >> u >> v >> d;
        e[u].push_back({v, d});
        e[v].push_back({u, d});
    }

    pq.push({0, 1});
    while(!pq.empty()){
        int u = pq.top().second, w = pq.top().first; pq.pop();

        if(vis[u]) continue;
        sum += w;
        vis[u] = 1;

        for(auto i:e[u]){
            int v = i.first, d = i.second;
            if(!vis[v]) pq.push({d, v});
        }
    }

    cout << sum << endl;
}

MST

一些題目:(要答案來找我)

Topological Sort

拓撲排序

Topo sort

什麼是 DAG?

Directed Acyclic Graph,有向無環圖。簡稱DAG

顧名思義,他是一個由有向邊 沒有環組成的圖

Topo sort

我們可以在一個DAG上作Topo sort

我們的目標是要找出一個由點組成的排序 (一串點的陣列)

而這個排序會有幾個條件要符合,就是哪個點要在哪個點前

 

舉例來說:我們有一個2要在3前面的條件,那在topo sort後

的這串點中,2必出現在3之前

Topo sort

因為一個 DAG 中一定有 Source (只有出沒有進的 node)

和 Sink(只進不出的 node)

 

簡單來說 Source的 in degree 是 0,Sink的 out degree 是 0

 

所以一個拓撲順序都是從 source 開始,從 sink 結束。

我們要用的演算法是 Kahn’s algorithm

我們要做的事:

  • 先找的圖中的每個 source
  • 對於每個 source 去做 BFS (多源BFS),走過的邊就從圖刪掉 (這也是順序)
  • 不斷找到新的 source,並加入 queue裡面
  • BFS,直到每個點都被走過了

Kahn’s algorithm

Kahn’s algorithm

時間複雜度:

O(n + m)

for (int i=1; i<=n; ++i){
    if (in[i] == 0) q.push(i); 
}
 
while (!q.empty()){
    int u = q.front(); q.pop();
    ans.push_back(u);
 
    for (int v:e[u]){
        --in[v];
        if (in[v] == 0) q.push(v);
    }
}

//ans 是 topo sort 的結果

Topo sort

來看看題目:

CSES - Course Schedule

給予我們一個有向圖 並且要找到一個topo sort的順序,如果沒有就輸出 "IMPOSSIBLE"

Topo sort

這只是一般的topo sort題目,只要多判斷有沒有環就好了

我們只要判說最後的答案有沒有包含全部的點,沒有的話就代表有環

#include <bits/stdc++.h>
#define endl '\n'
#define maxn 100005
#define int long long
using namespace std;
 
int n, m, in[maxn]; vector<int> e[maxn];
queue<int> q; vector<int> ans;
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v; cin >> u >> v;
        e[u].push_back(v);
        ++in[v];
    }
 
    for (int i=1; i<=n; ++i){
        if (in[i] == 0) q.push(i);
    }
 
    //topological sort
    while (!q.empty()){
        int u = q.front(); q.pop();
        ans.push_back(u);
 
        for (int v:e[u]){
            --in[v];
            if (in[v] == 0) q.push(v);
        }
    }
 
    if (ans.size() != n) cout << "IMPOSSIBLE" << endl;
    else{
        for (int i=0; i<ans.size(); ++i){
            if (i==ans.size()-1) cout << ans[i] << endl;
            else cout << ans[i] << " ";
        }
    }
}

Topo sort

來看看題目:

CSES - Game Routes

給予一個DAG,存找1到n有幾種走法,mod 1e9+7

什麼?圖上也可以dp?

Topo sort

假這我們有點 v,dp[v] 代表我到目前從1到v有幾種走法,

我們可以推論出一個dp式:

對於所有有向邊指向 v 的點 u,dp[v] += dp[u]

Topo sort

但是我們會遇到一個問題:

就是我們在更新 v 的時後,u 有可能還沒被更新

接下來會發現,我們只要由topo sort的順序來更新,就能保證 v 在更新的時候,u 已經被更新到了

Topo sort

dp 更新時候是 O(n+m),在這裡是往後更新,因為就不用存新的圖

#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define maxn 200005
using namespace std;
 
int n, m, dp[maxn];
int in[maxn]; vector<int> e[maxn], t;
queue<int> q;
const int p = 1e9+7;
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
 
    cin >> n >> m;
    for (int i=1; i<=m; ++i){
        int u, v; cin >> u >> v;
        e[u].push_back(v);
        ++in[v];
    }
 
    for (int i=1; i<=n; ++i){
        if (in[i] == 0) q.push(i);
    }
 
    while (!q.empty()){
        int x = q.front(); q.pop();
        t.push_back(x);
        for (int v:e[x]){
            --in[v];
            if (in[v] == 0) q.push(v);
        }
    }
 
    dp[1] = 1;
    for (int i:t){
        for (int v:e[i]) dp[v] = (dp[v] + dp[i]) % p; 
    }
 
    cout << dp[n] << endl;
}

Topo sort

一些題目:(要答案來找我)

耶你學會基礎圖論了

(那你要來上放課的進階圖論嗎 :P)
(也是我教 :P)

基礎圖論

By MLGnotCOOL