演算法 [7]

基礎圖論

INFOR38 學術 葉倚誠

講師:葉倚誠

  • 年次:116

  • 社團:建中資訊

  • 幹位:學術

  • 大括號不下放

My IG

INFOR38th IG

2026 IZCC 寒訓!

報名表單!

概述

什麼是圖?

離散數學中的圖,和日常生活中常聽到的圖不太一樣。

定義上, 是由若干頂點及連接兩頂點的邊所構成的圖形

 

圖可以解決的問題包括但不限於路徑、網路流、社群分析等

P.S. 圖論也是 APCS 檢定重要的命題範圍之一

圖 (Graph)

G = (V(G), E(G))

 

點(Vertex):又稱節點(Node)、頂點

邊(Edge):連結兩個點

V(G):點集,集合中有G中每一個點

E(G):邊集,V(G) 各點之間邊的集合

邊 (Edge)

以 (u, v) 數組表示,其中 u, v 為圖中的點

相鄰:若 (u, v) 存在,則 u, v 為相鄰

 

邊可以帶有方向、權重

邊權:邊上所帶的權重

有向圖:邊有方向;無向圖:邊沒有方向

 

重邊:有兩條邊的 (u, v) 相同

自環:u = v

簡單圖:沒有自環或重邊的圖

點 (Vertex)

度數:與一個點 v 相鄰的邊數量稱為度(Degree),記作 d(v)

有向圖的度數,分為出度、入度

出度:箭頭指離點;入度:箭頭指向點

 

點權:點上所帶的權重

路徑 (Path)

{V, E, V, E, V, ... , V}

 

由一連串點與邊所構成的集合,稱為路徑

 

簡單路徑:不重複經過同一個點的路徑

迴路:起點 = 終點的路徑

:既是簡單路徑又是迴路

連通 (Connected)

無向圖的連通

對於一張無向圖,如果有一條路徑可以從 u 連接到 v,我們就稱作 u, v 連通

連通圖:在圖中任取兩個點,這兩個點都是連通的

 

有向圖的連通:

強連通在圖中任取兩個點,這兩個點都可以互相通往對方

弱連通不論方向,在圖中任取兩個點,這兩個點都是連通的

完全圖 (Complete Graph)

完全圖:在無向簡單圖中,任兩個節點皆有邊相連

有向完全圖:在有向圖中,任意不同兩點都有兩條方向不同的邊

引理:握手定理

如果今天有 n 個人,兩兩握手數次

他們最後總共握手了 x 次

 

握手定理告訴我們,x 必定為偶數

 

因為當兩個人握手之後,總次數增加 2

所以當所有握手結束後,握手次數必定是 2 的倍數

 

我們可以利用這個邏輯發現對於一個無向簡單圖

各節點度數加總必定是偶數

Havel-Hakimi 演算法

1.  將度數陣列由大到小排序

2. 將度數最大的人消除,其他項的度數減一

3. 重複上述步驟

4. 若最後所有人的度數都是零,代表成功了

5. 若出現負度數,或最大度數大於其餘節點數,代表失敗了

#include <bits/stdc++.h>
using namespace std;

#define int long long
#define endl '\n'

bool f(const int &n, vector<int>& d) {
    int sum = 0;
    for (const int &x : d) {
        sum += x;
        if (x >= n) return false;
    }
    if (sum % 2 != 0) return false;

    for (int i = 0; i < n; ++i) {
        sort(d.begin() + i, d.end(), greater<int>());

        int d1 = d[i];
        if (d1 == 0) return true;
        if (d1 > (n - 1 - i)) return false;

        for (int j = 1; j <= d1; ++j) {
            d[i + j]--;
            if (d[i + j] < 0) return false;
        }
    }
    return true;
}

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

    int n;
    while (cin >> n && n) {
        vector<int> d(n);
        for (int i = 0; i < n; ++i) {
            cin >> d[i];
        }

        if (f(n, d)) {
            cout << "Yes" << endl;
        } else {
            cout << "No" << endl;
        }
    }
    
    return 0;
}

存圖

什麼是存圖?

存圖,顧名思義,就是把圖存下來

常用的做法有以下兩種:

  • 鄰接矩陣(Adjacency Matrix)

  • 鄰接陣列(Adjacency List

鄰接矩陣(Adjacency Matrix)

開一個二維陣列,例如 adj[u][v]

若為無邊權的圖,則 adj[u][v] = 1 or 0,代表有沒有邊

若為有邊權的圖,則 adj[u][v] 存邊權

 

如果是無向圖要存兩次(adj[u][v] 及 adj[v][u])

鄰接矩陣(Adjacency Matrix)

0 1 2 3
0 0 0 1 0
1 0 0 0 1
2 1 0 0 1
3 0 1 1 0

鄰接矩陣(Adjacency Matrix)

#include <bits/stdc++.h>
using namespace std;

#define endl '\n'

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

	int n, m;
	cin >> n >> m;
	vector<vector<bool>> adj(n, vector<bool>(n, 0));
	for (int i = 0; i < m; i++) {
		int u, v;
		cin >> u >> v;
		adj[u][v] = 1;
		adj[v][u] = 1;
	}

	return 0;
}

鄰接矩陣(Adjacency Matrix)

  • 難以處理有重邊的問題(除非重邊可以忽略)
  • 空間複雜度 O(n²),較大
  • 可以在 O(1) 時間中找到邊是否存在,及其權重(如果有)
  • 不適合處理稀疏圖 (若點數過大,空間會炸)

鄰接陣列(Adjacency List)

開一個陣列,索引值代表起點,元素存該點走向的所有點

無向圖要存兩次(做法同鄰接矩陣)

Graph = {
    {2},
    {3},
    {0, 3},
    {1, 2}
}

鄰接陣列(Adjacency List)

#include <bits/stdc++.h>
using namespace std;

#define endl '\n'

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

	int n, m;
	cin >> n >> m;
	vector<vector<int>> adj(n);
	for (int i = 0; i < m; ++i) {
		int u, v;
		cin >> u >> v;
		adj[u].push_back(v);
		adj[v].push_back(u);
	}

	return 0;
}

鄰接陣列(Adjacency List)

  • 無法直接查找 u, v 的邊
  • 難以處理有權重的問題
  • 空間複雜度較低
  • 無論是稀疏圖還是稠密圖都適合

哪一種比較好?

DFS(深度優先搜尋)

什麼是 DFS?

Depth First Search,中文為深度優先搜尋

 

這是一種用來遍歷或搜尋圖的演算法

深度優先代表會一條路走到底再換下一條走

 

可以用遞迴或是 Stack 來實作

0

4

6

5

1

2

3

DFS 步驟圖解

1. 從起始節點出發,例如 0

2. 不停往當前節點的相鄰節點走

3. 記錄走過的節點

4. 若已經沒有任何未走過的相鄰節點則折返

如此一來,可以走過所有與起點連通的節點

遍歷方式為走完一條路再折返

#include <bits/stdc++.h>
using namespace std;

#define endl '\n'

vector<vector<int>> adj;
vector<bool> visited;

void dfs(const int &cur) {
	cout << cur << endl;
    visited[cur] = true;
    for (int nxt : adj[cur]) {
        if (!visited[nxt]) {
            dfs(nxt);
        }
    }
}

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

    int n, m;
    cin >> n >> m;

    adj.assign(n, vector<int>());
    visited.assign(n, false);

    for (int i = 0; i < m; i++) {
        int u, v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

	dfs(0);

    return 0;
}
vector<vector<int>> adj;
vector<bool> visited;

void dfs(const int &start) {
    stack<int> st;
    st.push(start);

    while (!st.empty()) {
        int cur = st.top();
        st.pop();
        if (visited[cur]) continue;

		cout << cur << endl;
        visited[cur] = true;

        for (const auto &node : adj[cur]) {
            if (!visited[node]) {
                st.push(node);
            }
        }
    }
}

BFS(廣度優先搜尋)

什麼是 BFS?

 Breadth First Search,中文為廣度優先搜尋

 

與 DFS 最大的不同是,離起點越近的先走

每次都會嘗試走訪同一層的節點,直到全部走完

 

可以用 Queue 來實作

0

4

6

5

1

2

3

BFS 步驟圖解

1. 從起始節點出發,例如 0

2. 記錄所有相鄰節點

3. 往所有相鄰節點走(繼續記錄相鄰節點)

如此一來,可以走過所有與起點連通的節點

遍歷方式為先走同一層的節點優先

#include <bits/stdc++.h>
using namespace std;

#define endl '\n'

int n;
vector<vector<int>> adj;
vector<bool> visited;

void bfs(const int &start) {
    queue<int> q;

    visited[start] = 1;
    q.push(start);

    while (!q.empty()) {
        int cur = q.front();
        q.pop();

        for (const int &v : adj[cur]) {
            if (!visited[v]) {
                visited[v] = 1;
                q.push(v);
            }
        }
    }
}

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

    int n, m;
    cin >> n >> m;

    adj.resize(n);
    visited.assign(n, 0);

    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    bfs(0);

    return 0;
}

我們可以怎麼做?

先建立鄰接陣列,存圖

照前面的做法 BFS 搜索這張圖(起點為 1)

 

需要加上兩個陣列

一個存起點到各個電腦的路徑

(順便檢查起點與目標電腦是否連通)

 

如果連通,從目標電腦開始沿著原路徑走回去

開一個陣列存路徑

最後把陣列反轉(因為找路徑時倒著走),輸出

#include <bits/stdc++.h>
using namespace std;

#define endl '\n'
vector<vector<int>> adj;
vector<int> parent;
vector<int> dist;

int main() {
    int n, m;
    cin >> n >> m;

    adj.resize(n + 1);
    parent.resize(n + 1);
    dist.resize(n + 1);

    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    for (int i = 1; i <= n; ++i) dist[i] = -1;

    queue<int> q;
    q.push(1);
    dist[1] = 1;

    while (!q.empty()) {
        int curr = q.front();
        q.pop();

        if (curr == n) break;

        for (int neighbor : adj[curr]) {
            if (dist[neighbor] == -1) {
                dist[neighbor] = dist[curr] + 1;
                parent[neighbor] = curr;
                q.push(neighbor);
            }
        }
    }

    if (dist[n] == -1) {
        cout << "IMPOSSIBLE" << endl;
    } else {
        cout << dist[n] << endl;
        vector<int> path;
        for (int curr = n; curr != 0; curr = parent[curr]) {
            path.push_back(curr);
            if (curr == 1) break;
        }
        reverse(path.begin(), path.end());
        for (int i = 0; i < path.size(); i++) {
            cout << path[i] << (i == path.size() - 1 ? "" : " ");
        }
        cout << endl;
    }

    return 0;
}

The End

演算法放課回饋表單

演算法[7] 基礎圖論

By Ethan Yeh

演算法[7] 基礎圖論

  • 22