JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

圖:為連接關係建模

樹就是裝了輔助輪的圖——一個根,沒有環。把這些限制去掉,你就得到了**[[graph|圖]]**:這整個領域裡最靈活的結構,能為路網、朋友關係、網頁連結和任務依賴建模。這篇導讀給你詞彙(頂點、邊,有向/無向/帶權),以及在 C++ 裡存一張圖的兩種標準方法。

點與線

把所有細節剝掉,一張[[graph|圖]]不過就是*點和線*。點是頂點(也叫節點)——那些「東西」——線是——它們之間的關係。這就是全部定義,而它的空泛正是它的力量:每當你有*一些東西*以及*東西之間的連接*,你就有了一張圖。城市與道路。人與友誼。網頁與連結。任務與「必須先於」的箭頭。全是同一個抽象物件。

你已經認識一個特例:[[tree|樹]]不過是一張恰好有單一根、沒有環、且除一個節點外人人有一個父親的圖。圖把這三條規則統統扔掉。它可以有環(A→B→C→A)、有許多「父親」的節點、互不相連的孤島,甚至一個頂點連著自己的邊。這種自由,正是圖為何能把雜亂的現實世界建模得這麼好。

  (0)---(1)        an undirected graph, 5 vertices:
   |   / |         edges = { 0-1, 0-2, 1-2, 1-3, 3-4 }
   | /   |
  (2)   (3)---(4)

  note the cycle 0-1-2-0: trees can't do this, graphs can.
一張小小的無向圖。頂點是圓圈,邊是連線;環路 0-1-2-0 是一個環。

邊的三種風味

邊有幾種風味,而選對哪一種,就完成了把一個問題正確建模的一半。

  1. 無向 — 邊是雙向的。友誼:你認識我,我就認識你。畫成一條樸素的線。
  2. 有向 — 邊有方向,畫成箭頭。推特關注:你可以關注一個不回關你的人。A→B 不蘊含 B→A。
  3. 帶權 — 每條邊帶一個數字:距離、成本、行程時間。路網圖的邊以公里數加權。

這些可以自由組合。路網通常是無向且帶權的(路兩頭都能走,且無論哪個方向都是 5 公里)。專案排期是有向且無權的(「打地基」必須先於「砌牆」,沒得商量)。航班網絡是有向且帶權的(單程票,要花一筆票價)。在寫程式碼前先把風味叫準,能讓你不至於把錯的東西建了模。

儲存一張圖:鄰接串列 vs 鄰接矩陣

點和線是一個想法;計算機需要把它們作為資料。有兩種標準表示法。鄰接矩陣是一張 n×n 的方格,單元 `[i][j]` 在 i 到 j 有邊時為 1(或為權值),否則為 0。簡單,而且「i→j 之間有邊嗎?」是瞬時的 O(1) 查詢——但它總要用掉 n² 的空間,哪怕圖裡只有寥寥幾條邊。

[[adjacency-list|鄰接串列]]則為每個頂點只保留一份它的鄰居清單。它只用與邊數一樣多的空間,這正是它成為現實世界中那些大而稀疏的圖的預設選擇的原因(一座城市有數百萬個路口,但每個只接觸寥寥幾條路)。在 C++ 裡它無比自然:一個 `vector` 套著 `vector`。

graph:  0---1      adjacency LIST (a vector per vertex):
        |  /|        adj[0] = {1, 2}
        | / |        adj[1] = {0, 2, 3}
        2   3        adj[2] = {0, 1}
                     adj[3] = {1}

  same graph as a MATRIX (m[i][j]=1 if edge i-j):
        0 1 2 3        adjacency list:  ~edges of space
      0[0 1 1 0]       matrix:          always n*n space
      1[1 0 1 1]
      2[1 1 0 0]       sparse, big graph -> use the LIST
      3[0 1 0 0]
同一張圖的兩種存法。串列只存真實存在的邊;矩陣為每一對可能的頂點都留一格。
#include <vector>
int n = 4;
std::vector<std::vector<int>> adj(n);   // adjacency list

auto addEdge = [&](int u, int v) {       // UNDIRECTED:
    adj[u].push_back(v);                 //   add both directions
    adj[v].push_back(u);                 //   (drop one line if directed)
};
addEdge(0,1); addEdge(0,2); addEdge(1,2); addEdge(1,3);
C++ 中的鄰接串列。無向圖兩個方向都加;有向圖只加一個方向。