圖論演算法完整教材

Graph Algorithms — A Complete Guide

從圖的表示、走訪、最短路徑、最小生成樹、
網路流、進階結構,到真實世界的應用與解法

演算法教學系列

內附完整、可編譯的 C++17 實作(17 個核心演算法)

編譯方式:xelatex 連續執行兩次以解析目錄與交叉引用

「圖」(Graph)是電腦科學中最具表達力的資料結構之一。任何牽涉到「物件」與「物件之間關係」的問題,幾乎都能用圖來建模:社群網路的朋友關係、地圖上的道路、網頁之間的超連結、程式的相依性、晶片上的線路、金融交易的金流、分子的原子鍵結……。一旦把問題抽象成圖,我們就能套用一整套成熟、強大且效率經過嚴格證明的演算法來求解。

本教材的目標,是帶你從零開始、系統性地掌握圖論演算法。我們會回答四個核心問題:

  1. 如何表示圖? —— 鄰接矩陣、鄰接串列、邊串列各自的取捨。

  2. 如何走訪與分析圖? —— BFS、DFS 與其衍生的連通性、拓撲排序、環偵測、橋與割點。

  3. 如何解決經典圖問題? —— 最短路徑(Dijkstra、Bellman-Ford、Floyd-Warshall、A*)、最小生成樹(Kruskal、Prim)、網路流與二分匹配、最近共同祖先。

  4. 真實世界如何應用? —— 導航、社群推薦、詐欺偵測、排程、編譯器、網路路由,以及現代的圖神經網路(GNN)與圖資料庫。

每個演算法都包含:直觀說明、嚴謹定義、虛擬碼或數學式、手算範例搭配 TikZ 圖解、複雜度分析、常見陷阱,以及對應的真實應用。第 14 章提供一份完整、已實際編譯執行通過的 C++17 程式碼(涵蓋全部 17 個演算法),可直接複製使用。

閱讀建議:若你是初學者,請依章節順序閱讀,先打穩第 1–3 章(表示法與走訪)的基礎;若你準備面試或競賽,可直接跳到對應主題,並搭配第 13 章「解題策略:如何辨識題型」與第 15 章的練習題。

圖論基礎與術語

理解圖論演算法的前提,是先把術語講清楚。本章建立全書共用的語彙與數學記號。

什麼是圖

定義 1.1 (圖). 一個(graph)是一個有序對 \(G = (V, E)\),其中 \(V\)頂點(vertex / node)的有限集合,\(E \subseteq V \times V\)(edge)的集合。我們通常以 \(n = |V|\) 表示頂點數,\(m = |E|\) 表示邊數。

頂點代表「物件」,邊代表「物件之間的關係」。例如在社群網路中,頂點是使用者,邊是「好友」關係;在地圖中,頂點是路口,邊是道路。

上圖是一個 \(n=5\)\(m=6\) 的無向圖。

有向圖與無向圖

  • 無向圖(undirected graph):邊沒有方向,\((u,v)\)\((v,u)\) 是同一條邊。例如 Facebook 的好友關係(互相的)。

  • 有向圖(directed graph / digraph):邊有方向,\((u,v)\) 表示「從 \(u\) 指向 \(v\)」。例如 Twitter 的追蹤關係、網頁超連結、工作相依性。

加權圖與無權圖

若每條邊都附帶一個數值 \(w(u,v)\),稱為加權圖(weighted graph)。權重可表示距離、成本、時間、容量或機率。沒有權重(或視為權重皆為 1)的圖稱為無權圖(unweighted graph)。

權重可以是負數(例如代表退款、放熱反應)。一旦出現負權邊,Dijkstra 演算法就會失效,必須改用 Bellman-Ford。請見第 7 章。

重要術語速覽

術語 意義
鄰接 (adjacent) \((u,v) \in E\),稱 \(u\)\(v\) 相鄰。
度數 (degree) 與頂點相連的邊數。有向圖再分入度 (in-degree) 與出度 (out-degree)。
路徑 (path) 一連串相鄰頂點 \(v_0, v_1, \dots, v_k\)簡單路徑不重複經過頂點。
環 (cycle) 起點與終點相同的路徑(長度 \(\geq 1\),有向;\(\geq 3\),無向簡單環)。
連通 (connected) 無向圖中任兩頂點間都存在路徑。
強連通 (strongly connected) 有向圖中任兩頂點 \(u,v\) 互相可達。
子圖 (subgraph) \(V' \subseteq V\)\(E' \subseteq E\) 構成的圖。
完全圖 (complete graph) 任兩頂點都有邊,記為 \(K_n\),邊數 \(\binom{n}{2}\)
二分圖 (bipartite) 頂點可分為兩集合,邊只跨集合連接(無同側邊)。
樹 (tree) 連通且無環的無向圖,恰有 \(n-1\) 條邊。
DAG 有向無環圖(Directed Acyclic Graph),可做拓撲排序。
稠密/稀疏 稠密圖 \(m \approx n^2\);稀疏圖 \(m \approx n\)。決定資料結構選擇。

性質 1.1 (握手定理). 無向圖中所有頂點的度數總和等於邊數的兩倍: \[\sum_{v \in V} \deg(v) = 2|E|.\] 因為每條邊恰好替它的兩個端點各貢獻 1 度。由此可推得:度數為奇數的頂點必為偶數個。

圖的稀疏性與演算法選擇

「稀疏」還是「稠密」會直接影響你該用哪種表示法與演算法。一個直覺的判準:

\[\text{稠密} \iff m = \Theta(n^2), \qquad \text{稀疏} \iff m = O(n).\]

大多數真實世界的圖(社群網路、道路網、網頁圖)都是稀疏的——每個節點的鄰居數遠小於 \(n\)。這也是為什麼鄰接串列(第 2 章)幾乎是預設選擇。

圖的表示法

「如何在記憶體中表示一張圖」是所有圖演算法的起點。選錯表示法,輕則浪費記憶體,重則讓演算法慢上好幾個數量級。本章比較三種主流表示法。

鄰接矩陣 (Adjacency Matrix)

用一個 \(n \times n\) 的二維陣列 \(A\) 表示,其中 \[A[u][v] = \begin{cases} 1 & \text{若 } (u,v) \in E \text{(無權圖)}\\ w(u,v) & \text{若加權圖}\\ 0 \text{ 或 } \infty & \text{否則} \end{cases}\]

優點:查詢「\(u\)\(v\) 是否相鄰」為 \(O(1)\);實作簡單;適合稠密圖與需要頻繁查邊的演算法(如 Floyd-Warshall)。

缺點:空間固定為 \(O(n^2)\),對稀疏圖極度浪費;列舉某頂點的所有鄰居需 \(O(n)\)

int n;                                 // 頂點數
vector<vector<int>> A(n, vector<int>(n, 0));
A[u][v] = 1;                           // 加一條無向邊
A[v][u] = 1;

鄰接串列 (Adjacency List)

每個頂點維護一個「鄰居清單」。這是最常用的表示法,因為真實圖多為稀疏。

優點:空間為 \(O(n+m)\);列舉鄰居只花 \(O(\deg(v))\);適合稀疏圖與絕大多數走訪/最短路演算法。

缺點:查詢「\(u,v\) 是否相鄰」需 \(O(\deg(u))\)(非 \(O(1)\))。

int n;
vector<vector<pair<int,int>>> adj(n);  // adj[u] = {(鄰居, 權重), ...}
adj[u].push_back({v, w});              // 有向邊 u -> v
adj[v].push_back({u, w});              // 無向圖再加反向

邊串列 (Edge List)

直接存所有邊 \(\{(u, v, w)\}\)。本身不利於查鄰居,但對某些演算法(Kruskal 需要把邊依權重排序、Bellman-Ford 逐邊鬆弛)反而最自然。

struct Edge { int u, v, w; };
vector<Edge> edges;
edges.push_back({u, v, w});

三種表示法的比較

操作 / 性質 鄰接矩陣 鄰接串列 邊串列
空間複雜度 \(O(n^2)\) \(O(n+m)\) \(O(m)\)
\((u,v)\) 是否相鄰 \(O(1)\) \(O(\deg u)\) \(O(m)\)
列舉 \(u\) 的鄰居 \(O(n)\) \(O(\deg u)\) \(O(m)\)
加一條邊 \(O(1)\) \(O(1)\) \(O(1)\)
適合圖型 稠密 稀疏(最常用) 視演算法而定
代表演算法 Floyd- BFS, DFS, Kruskal,
Warshall Dijkstra, Prim Bellman-Ford

預設用鄰接串列。只有在 (1) 圖很稠密、(2) 演算法需要 \(O(1)\) 查邊、或 (3) 要做矩陣運算(如以矩陣冪計算路徑數)時,才考慮鄰接矩陣。

隱式圖 (Implicit Graph)

很多題目不會明確給你一張圖,而是「狀態空間」本身就是圖:

  • 格子地圖:每個格子是頂點,上下左右相鄰格是鄰居(牆壁除外)。

  • 字串轉換(Word Ladder):每個單字是頂點,差一個字母的單字相連。

  • 棋盤狀態:每個盤面是頂點,合法走步連到下一盤面。

這類問題不需建出整張圖,只要在走訪時即時生成鄰居即可。這是 BFS/DFS 最常見的實戰形式。

圖的走訪:BFS 與 DFS

走訪(traversal)是「有系統地拜訪圖中每個頂點」的過程,是幾乎所有圖演算法的骨架。兩大基石是廣度優先搜尋(BFS)與深度優先搜尋(DFS)。

廣度優先搜尋 (BFS)

BFS 從起點出發,一層一層向外擴張:先拜訪所有距離為 1 的頂點,再拜訪距離為 2 的,依此類推。它用一個佇列(queue, FIFO)來維護「待拜訪」的頂點。

演算法流程

  1. 將起點 \(s\) 放入佇列,標記 \(dist[s]=0\),其餘為未拜訪。

  2. 當佇列非空:取出隊首 \(u\),對每個未拜訪鄰居 \(v\),設 \(dist[v]=dist[u]+1\),並入列。

核心性質

定理 3.1 (BFS 最短路). 在無權圖中,BFS 求出的 \(dist[v]\) 就是從 \(s\)\(v\) 的最短路徑(邊數最少)長度。

直覺證明:BFS 按距離遞增的順序拜訪頂點,因此第一次抵達某頂點時,必定走的是最短路。

vector<int> bfs(const vector<vector<int>>& adj, int s) {
    vector<int> dist(adj.size(), -1);
    queue<int> q;
    dist[s] = 0; q.push(s);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u])
            if (dist[v] == -1) {          // 尚未拜訪
                dist[v] = dist[u] + 1;
                q.push(v);
            }
    }
    return dist;
}

複雜度:每個頂點入列/出列一次、每條邊檢查一次,故時間 \(O(V+E)\),空間 \(O(V)\)

BFS 的延伸應用

  • 多源 BFS:把所有起點一次放入佇列(如「腐爛的橘子」、火勢蔓延)。

  • 0-1 BFS:邊權只有 0 或 1 時,用雙端佇列取代優先佇列,達 \(O(V+E)\)(見第 7 章)。

  • 二分圖判定:BFS 交錯著色,若發現相鄰同色則非二分圖(見第 4 章)。

深度優先搜尋 (DFS)

DFS 採取「一路走到底,再回頭」的策略:從某頂點出發,盡量往深處探索,直到無路可走才回溯。它天然以遞迴(或顯式堆疊)實作。

void dfs(const vector<vector<int>>& adj, int u, vector<bool>& vis) {
    vis[u] = true;
    // ... 前序處理 u ...
    for (int v : adj[u])
        if (!vis[v]) dfs(adj, v, vis);
    // ... 後序處理 u ...
}

遞迴 DFS 在頂點數達數十萬時可能堆疊溢位(stack overflow)。深圖請改用顯式 stack 的迭代版本,或調大遞迴堆疊上限。

DFS 的時間戳與邊分類

DFS 過程中為每個頂點記錄「進入時間」\(tin[u]\) 與「離開時間」\(tout[u]\),可把邊分成四類(有向圖):

  • 樹邊(tree edge):DFS 樹中實際走的邊。

  • 回邊(back edge):指向祖先 —— 回邊的存在等價於有環

  • 前向邊(forward edge):指向已完成的子孫。

  • 橫向邊(cross edge):指向其他子樹。

這套「時間戳 + 邊分類」是後續環偵測、拓撲排序、強連通分量、橋與割點的共同基礎。

BFS 與 DFS 的比較

BFS DFS
資料結構 佇列 (FIFO) 堆疊 / 遞迴 (LIFO)
探索順序 由近到遠,逐層 一路到底再回溯
無權最短路 可以(保證最短) 不保證
記憶體 可能存整層,最差 \(O(V)\) 與遞迴深度相關 \(O(V)\)
典型用途 最短路、層序、最近目標 連通性、環、拓撲、SCC、回溯
時間複雜度 \(O(V+E)\) \(O(V+E)\)

口訣:「找最短用 BFS,找結構(環、連通、相依)用 DFS」。

連通性分析

「圖的哪些部分彼此相連?」是最基本的結構問題。本章涵蓋無向圖的連通分量、有向圖的強連通分量,以及二分圖判定。

連通分量 (Connected Components)

在無向圖中,連通分量是一個極大的頂點子集,集合內任兩點都互相可達。找出所有連通分量只需反覆從未拜訪的頂點啟動一次 DFS 或 BFS,並為它們標上同一個編號。

vector<int> components(const vector<vector<int>>& adj) {
    int n = adj.size(), cid = 0;
    vector<int> comp(n, -1);
    for (int s = 0; s < n; ++s) {
        if (comp[s] != -1) continue;
        stack<int> st; st.push(s); comp[s] = cid;
        while (!st.empty()) {
            int u = st.top(); st.pop();
            for (int v : adj[u])
                if (comp[v] == -1) { comp[v] = cid; st.push(v); }
        }
        ++cid;
    }
    return comp;
}

時間 \(O(V+E)\)。連通分量的計數在「朋友圈數量」、「島嶼數量」、「網路是否分裂」等問題中無所不在。

連通分量也可用第 9 章的並查集(DSU)增量維護——特別適合「邊會動態加入」的情境,例如逐步建線判斷網路何時連通。

二分圖判定 (Bipartite Check)

定義 4.1 (二分圖). 若頂點集可分割成兩個互斥集合 \(L, R\),使得每條邊都連接一個 \(L\) 點與一個 \(R\) 點(沒有同側邊),則 \(G\)二分圖

判定方法:用 BFS/DFS 做二著色。給起點塗色 0,鄰居塗相反色 1,再相反……若途中發現某條邊兩端同色,則不是二分圖。

定理 4.1. 一個圖是二分圖 \(\iff\) 它不含奇數長度的環

bool isBipartite(const vector<vector<int>>& adj) {
    int n = adj.size();
    vector<int> color(n, -1);
    for (int s = 0; s < n; ++s) {
        if (color[s] != -1) continue;
        queue<int> q; q.push(s); color[s] = 0;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : adj[u]) {
                if (color[v] == -1) { color[v] = color[u] ^ 1; q.push(v); }
                else if (color[v] == color[u]) return false; // 同色衝突
            }
        }
    }
    return true;
}

應用:排班(人 vs. 班次)、配對(求職者 vs. 職缺)、衝突偵測(兩群互斥),都先確認是否為二分圖,再套用第 11 章的二分匹配。

強連通分量 (SCC, Tarjan 演算法)

有向圖中,「連通」要分得更細。強連通分量(Strongly Connected Component)是極大的頂點子集,集內任兩點 \(u,v\)雙向可達\(u \to v\)\(v \to u\))。

Tarjan 演算法用一次 DFS 求出所有 SCC。核心是兩個時間戳:

  • \(disc[u]\)\(u\) 的「發現時間」。

  • \(low[u]\)\(u\) 經由 DFS 子樹 + 至多一條回邊,能回到的最早 \(disc\) 值。

搭配一個堆疊存放「目前尚未歸屬」的頂點。當 \(disc[u] = low[u]\) 時,\(u\) 是一個 SCC 的「根」,將堆疊彈出到 \(u\) 為止即構成一個 SCC。

定理 4.2. SCC 縮點後(把每個 SCC 收縮成一個超級節點)所得的圖必為DAG(稱為凝聚圖 / condensation)。這讓「先分層再處理」的策略成為可能。

複雜度\(O(V+E)\)。完整實作見第 14 章 TarjanSCC 類別。

應用:2-SAT 判定、編譯器的相互遞迴函式分組、社群網路的緊密群偵測、死結(deadlock)偵測。

橋與割點 (Bridges & Articulation Points)

在無向圖中,有些邊或點是「網路的咽喉」:

  • 割點(articulation point / cut vertex):移除後會使連通分量增加的頂點。

  • (bridge / cut edge):移除後會使連通分量增加的邊。

它們衡量網路的脆弱性——一座橋斷了,網路就分裂。同樣用 \(disc/low\) 時間戳,一次 DFS 即可求出:

  • \((u,v)\) 是橋 \(\iff low[v] > disc[u]\)\(v\) 的子樹無法繞過 \(u\) 回到更早的點)。

  • 非根頂點 \(u\) 是割點 \(\iff\) 存在子節點 \(v\) 使 \(low[v] \geq disc[u]\);根頂點是割點 \(\iff\) 它有 \(\geq 2\) 個 DFS 子樹。

應用:電信骨幹網路的單點故障分析、道路網的關鍵橋樑、社群網路的「橋接者」(連接兩個社群的關鍵人物)。完整實作見第 14 章 ArticulationBridge

拓撲排序 (Topological Sort)

問題定義

給定一個有向無環圖(DAG),拓撲排序是把所有頂點排成一個線性序列,使得對每條邊 \((u,v)\)\(u\) 都排在 \(v\) 之前。直覺上就是「相依關係的合法執行順序」。

拓撲排序只對 DAG 存在。若圖中有環,就不存在合法順序——這也是「偵測相依是否成循環」的方法。

Kahn 演算法(BFS 式)

核心觀念:入度為 0 的頂點沒有任何前置相依,可以最先處理。

  1. 計算每個頂點的入度,把入度 0 者全部入列。

  2. 反覆取出一個頂點 \(u\) 加入結果,並把它所有出邊「移除」(鄰居入度減 1);若鄰居入度降為 0 則入列。

  3. 若最終排序的頂點數 \(< n\),代表有環。

vector<int> topoSort(const vector<vector<int>>& adj) {
    int n = adj.size();
    vector<int> indeg(n, 0), order;
    for (int u = 0; u < n; ++u)
        for (int v : adj[u]) ++indeg[v];
    queue<int> q;
    for (int i = 0; i < n; ++i) if (indeg[i] == 0) q.push(i);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);
        for (int v : adj[u])
            if (--indeg[v] == 0) q.push(v);
    }
    return (int)order.size() == n ? order : vector<int>{}; // 空 = 有環
}

複雜度\(O(V+E)\)

DFS 式拓撲排序

另一種做法:對每個頂點做 DFS,在離開(後序)時把頂點推入堆疊,最後將堆疊反轉即為拓撲序。直覺是:一個頂點所有的後繼都處理完,它才被加入,故它必排在後繼之前。

若題目要求字典序最小的拓撲排序,把 Kahn 的普通佇列換成最小堆(priority_queue)即可。

應用

  • 建置系統 / 編譯相依:Makefile、Maven、npm 決定編譯順序。

  • 課程先修(LeetCode「課程表」):判斷能否修完所有課。

  • 試算表重算:儲存格公式相依的計算次序。

  • 任務排程:專案管理的工序安排(搭配關鍵路徑法)。

環偵測 (Cycle Detection)

「圖中有沒有環?」依有向/無向而有不同做法。環偵測是相依分析、死結偵測、資料一致性檢查的核心。

有向圖:DFS 三色法

為每個頂點維護三種狀態:

  • 白色(0,未拜訪)

  • 灰色(1,在當前 DFS 路徑上 / 遞迴堆疊中)

  • 黑色(2,已完成)

DFS 過程中,若遇到一條指向灰色頂點的邊(即回邊),就代表存在環。

bool dfsCycle(const vector<vector<int>>& adj, int u, vector<int>& color) {
    color[u] = 1;                        // 灰:進入遞迴
    for (int v : adj[u]) {
        if (color[v] == 1) return true;  // 回邊 -> 有環
        if (color[v] == 0 && dfsCycle(adj, v, color)) return true;
    }
    color[u] = 2;                        // 黑:離開
    return false;
}
bool hasCycle(const vector<vector<int>>& adj) {
    int n = adj.size();
    vector<int> color(n, 0);
    for (int i = 0; i < n; ++i)
        if (color[i] == 0 && dfsCycle(adj, i, color)) return true;
    return false;
}

不能只用「是否拜訪過」的二元旗標!指向黑色(已完成)頂點的邊是前向/橫向邊,不構成環。必須區分灰與黑。

無向圖:DFS 父節點法 或 並查集

無向圖中,DFS 時若遇到一個已拜訪且非父節點的鄰居,即代表有環:

bool dfsU(const vector<vector<int>>& adj, int u, int parent, vector<bool>& vis) {
    vis[u] = true;
    for (int v : adj[u]) {
        if (!vis[v]) { if (dfsU(adj, v, u, vis)) return true; }
        else if (v != parent) return true;   // 連到非父的已訪點 -> 環
    }
    return false;
}

另一種優雅做法是並查集:逐邊處理 \((u,v)\),若 \(u,v\) 已在同一集合則成環,否則合併。這正是 Kruskal 避免成環的機制(第 9 章)。

偵測負權環

在加權有向圖中,負權環會讓最短路徑無下界(繞圈無限變小)。Bellman-Ford 在第 \(V\) 次仍能鬆弛任何邊,即代表存在負權環(第 7 章)。

應用對照

情境 圖型 方法
死結偵測(資源等待圖) 有向 三色 DFS
試算表循環參照 有向 三色 DFS / 拓撲失敗
電路短路 / 冗餘連線 無向 並查集 / 父節點 DFS
套利機會(匯率) 加權有向 Bellman-Ford 找負權環

最短路徑演算法

最短路徑是圖論最重要、應用最廣的主題:導航軟體、網路路由、遊戲尋路、物流規劃都依賴它。本章依「圖的特性」介紹四種主力演算法,並給出選擇指南。

問題分類

圖的特性 適用演算法 時間複雜度
無權圖 BFS \(O(V+E)\)
邊權僅 0/1 0-1 BFS(雙端佇列) \(O(V+E)\)
非負權,單源 Dijkstra(二元堆) \(O((V+E)\log V)\)
含負權,單源 Bellman-Ford \(O(VE)\)
全源最短路 Floyd-Warshall \(O(V^3)\)
單點對 + 啟發 A* 視啟發函數

Dijkstra 演算法

核心思想

Dijkstra 解決非負權圖的單源最短路。它是貪婪演算法:維護一個「已確定最短距離」的集合,每次從「尚未確定」的頂點中挑出目前距離最小者,確定它,並用它去鬆弛(relax)鄰居。

定義 7.1 (鬆弛 Relaxation). 對邊 \((u,v)\) 權重 \(w\),若 \(dist[u] + w < dist[v]\),則更新 \(dist[v] \leftarrow dist[u] + w\)。這是所有最短路演算法的共同基本操作。

定理 7.1 (正確性的關鍵). 當所有邊權非負時,一旦某頂點被取出(其暫定距離在堆中最小),這個距離就是最終最短距離,不會再被更小的路徑更新。負權邊會破壞此假設,故 Dijkstra 不適用。

以優先佇列實作

用一個最小堆(min-heap)維護候選頂點,每次彈出距離最小者。

vector<long long> dijkstra(const vector<vector<pair<int,int>>>& adj, int s) {
    const long long INF = LLONG_MAX / 4;
    vector<long long> dist(adj.size(), INF);
    priority_queue<pair<long long,int>,
                   vector<pair<long long,int>>,
                   greater<>> pq;          // (距離, 頂點) 小根堆
    dist[s] = 0; pq.push({0, s});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;          // 過期項目,跳過
        for (auto [v, w] : adj[u])
            if (d + w < dist[v]) {
                dist[v] = d + w;
                pq.push({dist[v], v});
            }
    }
    return dist;
}

我們不刪除堆中的舊項目,而是在彈出時用 if (d > dist[u]) continue; 略過。這比實作「decrease-key」簡單得多,且不影響漸進複雜度。

手算範例

由 0 出發,依序確定:\(dist[0]{=}0 \to dist[2]{=}1 \to dist[1]{=}3\)(經 0→2→1,比直接 0→1 的 4 更短)\(\to dist[3]{=}4 \to dist[4]{=}7\)。最終 \(dist = [0, 3, 1, 4, 7]\)

重建路徑

只要在鬆弛時記錄前驅 \(parent[v] = u\),最後從終點沿 \(parent\) 回溯即可得到實際路徑。

0-1 BFS

當邊權只有 0 或 1 時,可用雙端佇列(deque)取代堆,達到 \(O(V+E)\):權重 0 的邊把鄰居推到隊(同層),權重 1 的推到隊(下一層)。

vector<int> zeroOneBFS(const vector<vector<pair<int,int>>>& adj, int s) {
    vector<int> dist(adj.size(), INT_MAX);
    deque<int> dq;
    dist[s] = 0; dq.push_front(s);
    while (!dq.empty()) {
        int u = dq.front(); dq.pop_front();
        for (auto [v, w] : adj[u])
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (w == 0) dq.push_front(v);
                else        dq.push_back(v);
            }
    }
    return dist;
}

應用:格子地圖中「有些移動免費、有些要花費 1」的最短代價問題。

Bellman-Ford 演算法

核心思想

Bellman-Ford 能處理負權邊,並能偵測負權環。它不貪婪,而是「暴力鬆弛」:對所有邊重複鬆弛 \(V-1\) 次。

定理 7.2. 在沒有負權環的圖中,任何最短路徑最多含 \(V-1\) 條邊。因此鬆弛所有邊 \(V-1\) 輪後,所有 \(dist\) 必收斂到最短值。若第 \(V\) 輪仍能鬆弛,代表存在負權環

struct Result { vector<long long> dist; bool negCycle; };
Result bellmanFord(int n, const vector<array<int,3>>& edges, int s) {
    const long long INF = LLONG_MAX / 4;
    vector<long long> dist(n, INF); dist[s] = 0;
    for (int i = 0; i < n - 1; ++i) {
        bool relaxed = false;
        for (auto& e : edges) {            // e = {u, v, w}
            if (dist[e[0]] == INF) continue;
            if (dist[e[0]] + e[2] < dist[e[1]]) {
                dist[e[1]] = dist[e[0]] + e[2];
                relaxed = true;
            }
        }
        if (!relaxed) break;               // 提早收斂
    }
    bool neg = false;
    for (auto& e : edges)
        if (dist[e[0]] != INF && dist[e[0]] + e[2] < dist[e[1]]) neg = true;
    return {dist, neg};
}

複雜度\(O(VE)\)。雖比 Dijkstra 慢,但能處理負權。SPFA 是其佇列優化版,平均較快但最差仍 \(O(VE)\)

把貨幣當頂點、匯率取對數負值當邊權,則「負權環」對應到一連串兌換後錢變多的套利機會——Bellman-Ford 正好能偵測。

Floyd-Warshall 演算法

所有頂點對之間的最短路。核心是極簡的動態規劃:考慮「只允許經過前 \(k\) 個頂點作中繼」的最短路,逐步放寬 \(k\)

\[d_{ij}^{(k)} = \min\!\left(d_{ij}^{(k-1)},\; d_{ik}^{(k-1)} + d_{kj}^{(k-1)}\right).\]

// d[i][j] 初始化為邊權,無邊為 INF,d[i][i]=0
for (int k = 0; k < n; ++k)
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            if (d[i][k] + d[k][j] < d[i][j])
                d[i][j] = d[i][k] + d[k][j];

三層迴圈中,\(k\) 必須在最外層!順序錯了會得到錯誤答案。另外鬆弛前要檢查 d[i][k]d[k][j] 非 INF,避免溢位。

複雜度:時間 \(O(V^3)\)、空間 \(O(V^2)\)。適合頂點數較小(\(V \lesssim 400\))但需要全源資訊的情境,也能用來求傳遞閉包(哪些點互相可達)。

A* 搜尋

A* 是 Dijkstra 的「有方向性」版本,常用於遊戲與地圖尋路。它用一個啟發函數 \(h(v)\) 估計 \(v\) 到終點的距離,以 \(f(v) = g(v) + h(v)\) 排序(\(g\) 為已知起點距離)。

  • \(h\)可採納(admissible,從不高估),A* 保證找到最短路。

  • 常見啟發:格子地圖用曼哈頓距離或歐氏距離。

  • \(h \equiv 0\) 時,A* 退化為 Dijkstra。

A* 透過優先探索「看起來更接近終點」的方向,大幅減少展開的節點數,但答案品質取決於啟發函數的設計。

最短路演算法選擇指南

決策樹:無權 → BFS;權重 0/1 → 0-1 BFS;非負權單源 → Dijkstra;有負權 → Bellman-Ford;要全源且 \(V\) 小 → Floyd-Warshall;單點對且有好啟發 → A*。

最小生成樹 (Minimum Spanning Tree)

問題定義

定義 8.1 (生成樹與最小生成樹). 給定一個連通、無向、加權\(G\),一棵生成樹(spanning tree)是包含全部 \(n\) 個頂點、且恰有 \(n-1\) 條邊的無環子圖。權重總和最小的生成樹稱為最小生成樹(MST)。

直覺:用最少的「成本」把所有點連起來。例如鋪設電纜連通所有城市、讓總線長最短。

性質 8.1 (兩個關鍵性質). 割性質(Cut Property):對任一將頂點分成兩半的「割」,橫跨割的最小權邊必屬於某棵 MST。這保證了貪婪選擇的正確性。
環性質(Cycle Property):任一環中權重最大的邊必屬於 MST。

Kruskal 演算法

策略:把所有邊依權重由小到大排序,依序嘗試加入;若一條邊的兩端點尚未連通(用並查集判斷)就加入,否則會成環,跳過。加滿 \(n-1\) 條邊即完成。

struct Edge { int u, v, w; };
long long kruskal(int n, vector<Edge> edges) {
    sort(edges.begin(), edges.end(),
         [](const Edge& a, const Edge& b){ return a.w < b.w; });
    DSU dsu(n);
    long long total = 0; int used = 0;
    for (auto& e : edges) {
        if (dsu.unite(e.u, e.v)) {         // 不成環才加入
            total += e.w;
            if (++used == n - 1) break;
        }
    }
    return total;
}

複雜度:排序主導,\(O(E \log E)\)。Kruskal 用邊串列最自然,特別適合稀疏圖

Prim 演算法

策略:類似 Dijkstra,從任一頂點開始「長樹」。維護一個優先佇列,每次挑出「連接樹與樹外、權重最小」的邊,把對應的新頂點納入樹中。

long long primMST(const vector<vector<pair<int,int>>>& adj) {
    int n = adj.size();
    vector<bool> inTree(n, false);
    priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
    long long total = 0; int taken = 0;
    pq.push({0, 0});                       // (權重, 頂點)
    while (!pq.empty() && taken < n) {
        auto [w, u] = pq.top(); pq.pop();
        if (inTree[u]) continue;
        inTree[u] = true; total += w; ++taken;
        for (auto [v, wt] : adj[u])
            if (!inTree[v]) pq.push({wt, v});
    }
    return total;
}

複雜度\(O((V+E)\log V)\)。Prim 用鄰接串列,較適合稠密圖(搭配鄰接矩陣的 \(O(V^2)\) 版本在稠密時甚至更快)。

Kruskal vs. Prim

Kruskal Prim
策略 全域挑最小邊(排序) 從一點長樹(局部擴張)
資料結構 並查集 + 邊串列 優先佇列 + 鄰接串列
複雜度 \(O(E\log E)\) \(O((V+E)\log V)\)
最適圖型 稀疏圖 稠密圖
易於處理「動態加邊」

應用

  • 網路 / 電纜 / 管線佈建:以最低總成本連通所有節點。

  • 群聚分析:刪除 MST 中最長的 \(k-1\) 條邊,可得到 \(k\) 個群(單連結聚類)。

  • 近似演算法:MST 是旅行推銷員問題(TSP)2-近似解的基礎。

  • 影像分割:以像素相似度為權重,用 MST 切割影像區域。

並查集 (Disjoint Set Union)

問題與動機

並查集(Disjoint Set Union, DSU / Union-Find)維護一組「不相交集合」,支援兩個操作:

  • find(x):回傳 \(x\) 所屬集合的代表元(root)。

  • unite(x, y):合併 \(x\)\(y\) 所在的兩個集合。

它是 Kruskal、連通性維護、環偵測的幕後功臣,也是動態連通問題的標準解。

兩大優化:路徑壓縮 + 按秩合併

樸素實作的樹可能退化成鏈,使 find\(O(n)\)。兩個優化讓它幾乎變成常數:

  • 路徑壓縮(path compression):find 時把沿途節點直接掛到根下。

  • 按秩 / 按大小合併(union by rank/size):把較矮的樹掛到較高的樹下,避免長高。

定理 9.1 (近乎常數). 同時採用兩種優化後,\(m\) 次操作的總時間為 \(O(m\,\alpha(n))\),其中 \(\alpha\) 是反阿克曼函數,在所有實際輸入下 \(\alpha(n) \leq 4\)。可視為「攤還常數時間」。

class DSU {
    vector<int> parent, rank_;
public:
    DSU(int n) : parent(n), rank_(n, 0) {
        iota(parent.begin(), parent.end(), 0);  // 各自成集
    }
    int find(int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];       // 路徑壓縮(折半)
            x = parent[x];
        }
        return x;
    }
    bool unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false;              // 已在同集(成環)
        if (rank_[rx] < rank_[ry]) swap(rx, ry);
        parent[ry] = rx;
        if (rank_[rx] == rank_[ry]) ++rank_[rx];
        return true;
    }
    bool same(int x, int y) { return find(x) == find(y); }
};

應用

  • Kruskal MST:判斷加邊是否成環。

  • 動態連通性:逐步加邊,隨時查詢兩點是否連通。

  • 連通分量計數:合併同時維護分量數。

  • 離線查詢:如「島嶼數量 II」逐格加陸地。

  • 帶權並查集:可維護到根的相對關係(如「食物鏈」種類推理)。

DSU 擅長「加邊」與「查連通」,但不支援高效刪邊。若題目會刪邊,常見手法是「時間倒流」——離線把操作反轉成加邊。

網路流 (Network Flow)

網路流是圖論中極具威力的建模工具:許多看似無關的問題(匹配、分配、切割、排程)都能化約成「最大流」來求解。

最大流問題

定義 10.1 (流網路). 一個流網路是有向圖 \(G=(V,E)\),每條邊 \((u,v)\) 有非負容量 \(c(u,v)\),並指定源點 \(s\)匯點 \(t\)。一個 \(f\) 需滿足:(1) 容量限制 \(0 \leq f(u,v) \leq c(u,v)\);(2) 流量守恆:除 \(s,t\) 外,每個頂點流入 \(=\) 流出。最大流即從 \(s\) 送到 \(t\) 的最大總流量。

Ford-Fulkerson 與 Edmonds-Karp

核心觀念:反覆在殘餘網路(residual graph)中尋找一條從 \(s\)\(t\)增廣路徑(augmenting path),沿途增加流量,直到找不到為止。

  • 殘餘容量:邊 \((u,v)\) 還能再送的量 \(= c(u,v) - f(u,v)\)。每條正向邊都伴隨一條反向邊,容量初始為 0,用來「反悔」先前的流。

  • Ford-Fulkerson:用 DFS 找增廣路,整數容量下會終止,但複雜度依賴流值。

  • Edmonds-Karp:固定用 BFS最短增廣路,複雜度 \(O(VE^2)\),與容量大小無關。

struct MaxFlow {
    struct E { int to, rev, cap; };
    vector<vector<E>> g;
    MaxFlow(int n) : g(n) {}
    void addEdge(int u, int v, int cap) {
        g[u].push_back({v, (int)g[v].size(), cap});
        g[v].push_back({u, (int)g[u].size()-1, 0}); // 反向邊
    }
    int run(int s, int t) { /* BFS 找增廣路並更新殘餘圖,見第 14 章 */ }
};

最大流最小割定理

定理 10.1 (Max-Flow Min-Cut). 在任一流網路中,從 \(s\)\(t\)最大流值,恰等於分離 \(s\)\(t\)最小割(移除後使 \(s\) 無法到 \(t\) 的邊容量總和最小者)的容量。

這個對偶關係極為深刻:求「最多能送多少」等價於求「最便宜的切斷方式」。它把流問題與割問題(影像分割、可靠度分析)連結起來。

建模的藝術:問題化約

網路流真正的威力在於把其他問題翻譯成流

  • 二分匹配:源點連左部、右部連匯點,所有容量設 1,最大流即最大匹配(見下章)。

  • 帶下界 / 多源多匯:加超級源匯轉換。

  • 最小成本最大流:邊再附成本,用 SPFA/Bellman-Ford 找最便宜增廣路——用於指派、運輸問題。

  • 專案選擇 / 最大權閉合子圖:化為最小割。

二分匹配 (Bipartite Matching)

問題定義

給定二分圖(左部 \(L\)、右部 \(R\)),匹配是一組沒有共用端點的邊。最大匹配即邊數最多的匹配。典型情境:把求職者配到職缺、學生配到專題、計程車配到乘客。

匈牙利演算法(增廣路)

核心是增廣路徑:一條交替走「未匹配邊、已匹配邊」、且兩端都是未匹配點的路徑。沿此路徑翻轉所有邊的匹配狀態,匹配數就 \(+1\)。對每個左部點嘗試找增廣路即可。

bool tryMatch(int u, vector<vector<int>>& adj,
              vector<int>& matchR, vector<bool>& used) {
    for (int v : adj[u]) {
        if (used[v]) continue;
        used[v] = true;
        if (matchR[v] == -1 || tryMatch(matchR[v], adj, matchR, used)) {
            matchR[v] = u;                 // 配對 / 重新配對
            return true;
        }
    }
    return false;
}

複雜度\(O(V \cdot E)\)。資料量大時可用 Hopcroft-Karp 達 \(O(E\sqrt{V})\)

定理 11.1 (König 定理). 在二分圖中,最大匹配數 \(=\) 最小頂點覆蓋數。又由 König 可推得最大獨立集 \(= n -\) 最大匹配。這些對偶關係讓匹配能解決覆蓋與獨立集問題。

應用

  • 指派問題:員工↔︎任務、課程↔︎教室、機器↔︎工件。

  • 排班:醫師↔︎值班時段。

  • 推薦 / 廣告:在限制下把廣告位配給廣告主(常用最小成本流的加權版)。

進階主題

本章彙整幾個常在競賽與實務出現的進階結構,著重觀念與應用,完整實作見第 14 章。

最近共同祖先 (LCA)

在有根樹中,兩節點 \(u,v\)最近共同祖先是同時為兩者祖先、且深度最大的節點。它能高效回答「樹上兩點距離」、「路徑查詢」等問題。

倍增法(Binary Lifting):預處理 \(up[k][v] =\)\(v\) 的第 \(2^k\) 個祖先」。查詢時先把較深的點上提到同深度,再一起往上跳。

  • 預處理 \(O(V \log V)\),每次查詢 \(O(\log V)\)

  • 樹上兩點距離 \(= depth[u] + depth[v] - 2\,depth[\text{lca}(u,v)]\)

應用:階層分類、family tree、網路拓撲的共同上游、版本控制的共同祖先 commit。

歐拉路徑與歐拉迴路

歐拉路徑是「不重複地走過每一條邊恰好一次」的路徑;若起終點相同則為歐拉迴路。這正是著名的「七橋問題」。

  • 無向連通圖存在歐拉迴路 \(\iff\) 所有頂點度數皆偶數

  • 存在歐拉路徑(非迴路)\(\iff\) 恰有兩個奇數度頂點。

  • Hierholzer 演算法可在 \(O(E)\) 內構造。

應用:DNA 片段重組、郵差路線、一筆畫問題。

最小生成樹的延伸

  • 次小生成樹:在 MST 基礎上換一條邊。

  • 有向最小生成樹(樹形圖):朱劉演算法(Chu-Liu/Edmonds)。

  • Steiner 樹:只需連通指定子集(NP-hard,常用近似)。

圖著色與最大團

圖著色:給頂點塗色使相鄰異色,最少用幾種色(色數)是 NP-hard。應用於排程、暫存器配置、頻率分配。最大團(互相皆相鄰的最大頂點集)同為 NP-hard,但在實務上常用啟發式或位元優化暴搜。

現代延伸:圖神經網路 (GNN)

近年圖機器學習快速崛起。圖神經網路(GNN)透過訊息傳遞(message passing)學習節點 / 邊 / 整圖的向量表示:每個節點反覆「聚合鄰居資訊 → 更新自身狀態」,多輪後即捕捉到局部結構。

  • 代表架構:GCN(圖卷積)、GraphSAGE(取樣聚合)、GAT(注意力)。

  • 任務型態:節點分類、連結預測、整圖分類。

  • 應用:分子性質預測、推薦系統、詐欺偵測、交通預測、知識圖譜補全。

傳統圖演算法靠明確規則求精確解(如最短路);GNN 則從資料學習模式,擅長有雜訊、需預測的場景。實務上兩者常互補:先用圖演算法萃取特徵(中心性、社群、距離),再餵給機器學習模型。

解題策略:如何辨識題型

面對一道新題,最關鍵的是把問題翻譯成圖辨識題型。本章提供一套系統化的思考流程。

第一步:辨識圖的結構

問自己四個問題:

  1. 頂點與邊是什麼? 物件是頂點,關係是邊。格子?狀態?單字?

  2. 有向還是無向?

  3. 有權還是無權?權重可為負嗎?

  4. 圖是明確給定,還是隱式狀態空間?

第二步:題型 → 演算法對照表

當你看到…… 多半要用……
無權圖最短路 / 最少步數 BFS
邊權 0/1 的最短代價 0-1 BFS
非負權最短路 Dijkstra
含負權 / 偵測套利 Bellman-Ford
所有點對距離(\(V\) 小) Floyd-Warshall
連通分量 / 島嶼數 / 朋友圈 DFS/BFS 或 並查集
相依排序 / 課程表 / 編譯順序 拓撲排序
偵測(有向)環 / 死結 三色 DFS
最小成本連通所有點 MST(Kruskal / Prim)
動態加邊查連通 並查集
指派 / 配對 / 排班 二分匹配
最大吞吐 / 最小切斷 最大流 / 最小割
樹上兩點距離 / 共同祖先 LCA(倍增)
互斥兩群判定 二分圖著色

第三步:常見陷阱檢查表

  • 負權誤用 Dijkstra(會給錯但不報錯)。

  • 遞迴 DFS 在大圖堆疊溢位——改迭代。

  • Floyd-Warshall 的 \(k\) 沒放最外層

  • 環偵測只用二元 visited,沒區分灰/黑

  • 無向圖環偵測忘了排除父節點

  • 最大流忘了加反向邊

  • 距離用 int溢位——大圖改 long long,且 INF 取 LLONG_MAX/4 避免相加溢位。

真實世界的應用

圖演算法不是學術玩具——它們是現代科技基礎建設的核心。本章蒐集 2026 年產業實例。

導航與地圖

Google Maps、Waze 的路線規劃本質是加權最短路。實務上不直接跑 Dijkstra(太慢),而是用雙向搜尋A* + 地標啟發、以及收縮層級(Contraction Hierarchies)等預處理技術,把全國路網的查詢壓到毫秒級。即時路況則把邊權動態更新。

社群網路

  • 好友推薦:「你可能認識的人」用 BFS 找二度人脈、共同好友數。

  • 影響力 / 排名:PageRank(特徵向量中心性的變體)衡量節點重要性。

  • 社群偵測:用連通分量、Louvain、標籤傳播找出緊密群體。

  • Netflix 以自研「Graph Abstraction」服務社交圖與服務拓撲,尖峰處理近每秒千萬次操作、橫跨 650 TB 圖資料。

詐欺偵測與金融

詐欺本質是連結問題——詐騙集團以「環狀金流、共用裝置、共用身分」運作,傳統逐筆分析難以察覺。圖資料庫(如 Neo4j)用以下技巧揭露詐欺網:

  • 連通分量 / 社群偵測找出詐欺環。

  • PageRank / 中心性找出異常具影響力的帳戶(人頭中樞)。

  • 節點相似度(Jaccard)偵測「共用 SSN / Email / 電話」的模仿帳戶。

  • 多跳遍歷追蹤洗錢的資金流向。

Neo4j 宣稱以圖比關聯式資料庫快上千倍揭露此類模式。

推薦系統

電商與串流以二分圖(使用者↔︎商品)建模。協同過濾透過圖遍歷找「和你相似的人也喜歡的東西」。一個典型 Cypher 查詢的精神:找「我喜歡的電影 → 也喜歡它的其他人 → 他們喜歡但我還沒看的電影」,依出現次數排序推薦。

基礎設施與供應鏈

  • 網路路由:OSPF 用 Dijkstra 計算最短路徑樹;BGP 處理跨自治系統路由。

  • 供應鏈:以圖建模多階供應商相依,分析斷點風險(割點 / 橋)與替代路徑。Google BigQuery Graph 讓企業在 PB 級資料上做多跳相依分析。

  • 電網 / 電信:割點分析找單點故障;最大流評估容量。

編譯器、建置與排程

  • 相依解析:套件管理(npm、Maven)、建置系統用拓撲排序決定順序、用環偵測抓循環相依。

  • 暫存器配置:以圖著色決定變數該放哪個暫存器。

  • 指令排程 / 關鍵路徑:DAG 上的最長路徑決定專案最短工期。

生物資訊與其他

DNA 定序的片段重組用歐拉路徑(de Bruijn 圖);蛋白質交互作用網路、代謝路徑分析、藥物分子的性質預測(GNN)都以圖為核心。此外,知識圖譜(Knowledge Graph)支撐了搜尋引擎與大型語言模型的 GraphRAG 檢索。

完整 C++ 實作

本章提供一份完整、可直接編譯執行的 C++17 程式碼,涵蓋全書 17 個核心演算法。此程式碼已實際編譯(g++ -std=c++17 -O2)並執行驗證通過。

走訪與結構:BFS、DFS、拓撲排序(Kahn)、有向環偵測、連通分量、強連通分量(Tarjan)、割點與橋。
最短路:Dijkstra、0-1 BFS、Bellman-Ford、Floyd-Warshall。
生成樹與集合:Kruskal、Prim、並查集(DSU)。
流與匹配與樹:Edmonds-Karp 最大流、二分匹配、LCA(倍增)。

編譯與執行

g++ -std=c++17 -O2 -Wall -Wextra -o graph_algorithms graph_algorithms.cpp
./graph_algorithms

原始碼

執行輸出(節錄)

1. BFS shortest distance (unweighted)
  dist from 0 = 0 1 1 2 3 4
3. Topological sort (Kahn)
  order: 0 1 2 3 5 4
5. SCC (Tarjan): components count = 2
  comp[v] = 1 1 1 0 0 0
6. Articulation points & bridges
  articulation = 2 3 4
  bridges = (3,4) (2,3)
8. Dijkstra: dist from 0 = 0 3 1 4 7
10. Bellman-Ford: dist from 0 = 0 2 7 4 -2  (no negative cycle)
12. Kruskal MST: total weight = 14
13. Prim MST:    total weight = 14
15. Edmonds-Karp max flow s->t = 23
16. Bipartite matching = 4
17. LCA(6,4) = 1 dist = 3 ; LCA(6,5) = 0 dist = 5

練習題

以下練習依難度分級。建議先獨立思考「該用哪個演算法、如何建圖」,再動手實作。多數題目可在 LeetCode、Codeforces 找到對應題。

入門(走訪與連通)

  1. 島嶼數量:給 0/1 格子地圖,求相連陸地塊數。(DFS/BFS 或 並查集)

  2. 無權最短路:求迷宮起點到終點最少步數。(BFS)

  3. 二分圖判定:判斷是否可二著色。(BFS 二著色)

  4. 朋友圈:給朋友關係矩陣求圈數。(並查集)

  5. 克隆圖:深拷貝一張無向圖。(DFS + 雜湊表)

進階(排序與最短路)

  1. 課程表 II:輸出可行修課順序,或判斷不可能。(拓撲排序)

  2. 網路延遲時間:信號傳到所有節點的最短時間。(Dijkstra)

  3. K 站中轉最便宜航班:帶限制的最短路。(修改版 Dijkstra / Bellman-Ford)

  4. 路徑最小體力消耗:邊權為高度差,最小化路徑最大邊。(Dijkstra 變體 / 二分 + BFS)

  5. 連接所有點的最小費用:曼哈頓距離為權的 MST。(Prim / Kruskal)

挑戰(流、匹配、進階)

  1. 最大二分匹配:求職者與職缺配對。(匈牙利 / 最大流)

  2. 逃生問題:多源到邊界的最大不相交路徑。(最大流 / 最小割)

  3. 關鍵連接(橋):找出網路中所有橋。(Tarjan)

  4. 樹上路徑查詢:多次詢問兩點距離。(LCA 倍增)

  5. 重新規劃路線:使所有點能到達 0 的最少改向邊數。(DFS / BFS 建雙向圖)

  6. 套利偵測:給匯率表判斷是否存在套利。(Bellman-Ford 找負權環)

做題時養成習慣:先在紙上畫小範例、手動模擬演算法,再寫程式。對拍(brute force vs. 正解)能有效抓出邊界錯誤。

總結與延伸閱讀

複雜度速查表

演算法 時間 空間
BFS / DFS \(O(V+E)\) \(O(V)\)
拓撲排序(Kahn) \(O(V+E)\) \(O(V)\)
連通分量 \(O(V+E)\) \(O(V)\)
Tarjan SCC \(O(V+E)\) \(O(V)\)
割點 / 橋 \(O(V+E)\) \(O(V)\)
Dijkstra(堆) \(O((V+E)\log V)\) \(O(V)\)
0-1 BFS \(O(V+E)\) \(O(V)\)
Bellman-Ford \(O(VE)\) \(O(V)\)
Floyd-Warshall \(O(V^3)\) \(O(V^2)\)
Kruskal \(O(E\log E)\) \(O(V)\)
Prim(堆) \(O((V+E)\log V)\) \(O(V)\)
並查集 \(O(\alpha(n))\)/操作 \(O(V)\)
Edmonds-Karp \(O(VE^2)\) \(O(V+E)\)
二分匹配(匈牙利) \(O(VE)\) \(O(V+E)\)
LCA(倍增) 預處理 \(O(V\log V)\),查詢 \(O(\log V)\) \(O(V\log V)\)

學習路徑建議

  1. 打地基:徹底搞懂鄰接串列、BFS、DFS。八成的圖題都建立在這三者之上。

  2. 連通性家族:連通分量、二分圖、拓撲排序、環偵測——它們都是 DFS/BFS 的變體。

  3. 最短路家族:先 Dijkstra,再 Bellman-Ford 與 Floyd-Warshall,理解何時用哪個。

  4. 生成樹與並查集:Kruskal/Prim/DSU 一起學,互相呼應。

  5. 進階:網路流、匹配、LCA、Tarjan,依需求深入。

  6. 現代延伸:圖資料庫(Neo4j、Cypher)、圖機器學習(GNN)。

延伸閱讀

  • Introduction to Algorithms(CLRS),第 22–26 章——圖演算法的權威教科書。

  • Competitive Programming(Halim 兄弟)——競賽導向、實作豐富。

  • Algorithm Design(Kleinberg & Tardos)——強調建模與證明。

  • Graph Algorithms(O’Reilly, Needham & Hodler)——以 Neo4j / Spark 講實務圖分析。

  • cp-algorithms.com——線上免費、涵蓋完整的競賽演算法參考。

  • PyTorch Geometric 官方教學——入門圖神經網路(GNN)的最佳起點。

圖論演算法的迷人之處,在於它把五花八門的真實問題統一到「頂點 + 邊」的優雅框架下。一旦你能熟練地「把問題看成圖」,許多原本棘手的難題都會迎刃而解。祝你學習愉快,並在真實專案中發揮這套強大的工具!