# 基礎圖論 (四): 圖的表示與儲存 之前都是在講偏數學的觀點。這篇要把目光移回電腦上,在電腦中要儲存一張圖,需要使用相對應的資料結構,才能用點腦處理圖論問題 備註 : 這篇在沒有特別註明的情況下,圖都是 simple graph 備註 : 因為這是競程筆記,所以實作一定是 C/C++ ## 鄰接矩陣 Adjacency Matrix ### 無向簡單圖 Undirected Simple Graph 從[此系列的第一篇](https://hackmd.io/@ShanC/basic-graph-theory-1)就一直強調,邊是由 relation 定義。離散數學的課本也說,relation 可以用矩陣來表示。根據此性質,一張 simple graph 用矩陣來表示是很正常的一件事。然而,要將圖以矩陣表示,每個節點就必須有編號,才能使得每個行列具有意義 我們可以定義矩陣 $A_{i, j}=\begin{cases} 1 \text{, if } (i, j)\in E\\ 0 \text{, otherwise} \end{cases}$。對於以下右圖而言,他可以用右邊的矩陣表示 <center> <img src="https://hackmd.io/_uploads/ByUJrU8Deg.png" style="margin: 0 auto; display: block; width: 400px"> </center> $~$ 這個矩陣是對稱的。此外,若允許自環 (loop),鄰接矩陣仍然可以表達,但是多重邊卻難以用矩陣表達,有時數字代表邊數,但**很多時候要視情況而定**。所以大多時候討論的都會是簡單圖 ### 有向簡單圖 Directed Simple Graph 想當然,有向圖是由 function 表示,因此也可以用鄰接矩陣表示,但矩陣不一定會對稱 <center> <img src="https://hackmd.io/_uploads/SySd9IUveg.png" style="margin: 0 auto; display: block; width: 400px"> </center> ### 程式實作 沒有很難,就只是二維陣列而已 ```cpp int n = 50; // 節點數 int graph[n][n]; ``` 空間複雜度沒什麼好說的,就是 $O(n^2)$,因此若要以鄰接矩陣確認整張圖的樣貌,時間也是 $O(n^2)$ ### 鄰接矩陣的缺點 在鄰接矩陣中,如果不是完全圖,那就一定會有為 $0$ 的元素。但大多數的狀況是,我們其實不需要知道「哪些點對有連邊」,因此儲存 $0$ 的那些空間顯然很浪費。若今天存的是一張邊數與點數差不多的圖,那麼這個效果會更明顯。可以想像,若有一個邊數是 $O(n)$ 的圖,但我們卻每次都用 $O(n^2)$ 的時間去遍歷整個資料結構,那麼一定會花非常多時間 ### 鄰接矩陣的優點 矩陣畢竟是一個數學物件,具有數學上的一些運算性質,像是行運算、列運算、冪次、生成函數或是特徵值等都是值得探討的事情,因此也很重要。只是電腦實作上我們更關心連通的狀況,相較之下,鄰接串列比較符合我們寫程式的需求 ## 圖同構 Isomorphism 由於矩陣與圖本質上是等價的,大多圖的性質都可以從鄰接矩陣找到。其實這算是代數圖論的範疇,不是競程會去討論的內容,以下就舉幾個例子讓你有點感覺 ### 定義 給定兩張圖 $A$ 與 $B$,若我們可以找出一個 bijection $f: V(A)\rightarrow V(B)$ 使得 $(u, v)\in E(A)$ 若且唯若 $(f(u), f(v))\in E(B)$,則稱為同構 ||事實上,群論中的同構也差不多是這樣定義|| ### 舉例說明 如下圖,$G$ 與 $H$ 就是同構 <center> <img src="https://hackmd.io/_uploads/ryWVpIUwgx.png" style="margin: 0 auto; display: block; width: 400px"> </center> 因為我們只要將 $G$ 的 $1$ 對應到 $H$ 的 $4$、$G$ 的 $2$ 對應到 $H$ 的 $2$、$G$ 的 $3$ 對應到 $H$ 的 $3$、$G$ 的 $4$ 對應到 $H$ 的 $1$ 他們的鄰接矩陣分別為 $A_G=\left[ \begin{array}{cccc} 0 & 0 & 1 & 0\\ 0 & 0 & 1 & 1\\ 1 & 1 & 0 & 0\\ 0 & 1 & 0 & 0\\ \end{array} \right]$ 與 $A_H=\left[ \begin{array}{cccc} 0 & 1 & 0 & 0\\ 1 & 0 & 1 & 0\\ 0 & 1 & 0 & 1\\ 0 & 0 & 1 & 0\\ \end{array} \right]$ 如上,若要判別是否同構,可以將 $A_G$ 的行與列的 $1$ 跟 $4$ 做交換,就可以得到相同的矩陣 ### 判斷同構 若要設計演算法來判別,可以窮舉所有的節點編號的排列組合,只要其中一種會使得矩陣長的一樣,則就是同構 ## 邊的數量 Number of Edges 完全圖擁有一張圖最多的邊數。有了資料結構,要推出一張 $n$ 個節點的圖最多的邊數就不是個問題。以下假設無向圖有 $n$ 個節點 ### 方法一 : 用矩陣計算邊數 鄰接矩陣總共有 $n^2$ 個元素,每個元素都代表行與列是否有邊相連。由於圖不存在自環,扣掉自環的邊數有 $n$ 條邊。因矩陣是對稱的,$r$ 連到 $c$ 與 $c$ 連到 $r$ 其實是相同一條邊,所以最後要除 $2$。因此得到共有 $\cfrac{n^2-n}{2}=\cfrac{n(n-1)}{2}$ 條邊 ### 方法二 : 算組合數 注意到每兩點之間必有一條邊,因此是 $C^{n}_{2}=\cfrac{n(n-1)}{2}$ ## 鄰接串列 Adjacency List 有人稱這個為鄰接表,反正都一樣 ### 說明 在這個資料結構裡面,我們只記錄兩節點的連接狀況,有邊就紀錄,沒邊就不理他。對於每個節點 $u$,鄰接串列只記錄 $u$ 有連到的節點 $v$。以下就給個圖,自行意會一下吧! <center> <img src="https://hackmd.io/_uploads/rk4W2LuDeg.png" style="margin: 0 auto; display: block; width: 400px"> </center> ### 程式實作 傳統上應該要用 linked list,但是因為這是競程筆記,所以我們還是用一些 vector 把他們都串起來就好。實際上,這算是一個二維陣列,只是因為 vector 可以調整大小、插入元素的緣故,因此這樣存就相當於用 linked list 存,效果差不多,只是空間可能會大一點,但還是跑得動 ```cpp int n = 50; // 節點數 vector<int> g[n]; ``` 若我們想要存入上面的例子,那可以做以下的操作 ```cpp g[1].push_back(2), g[1].push_back(4); g[2].push_back(1), g[2].push_back(6); g[3].push_back(5), g[3].push_back(6); g[4].push_back(1), g[4].push_back(5); g[5].push_back(3), g[5].push_back(4), g[1].push_back(6); g[6].push_back(2), g[6].push_back(3), g[6].push_back(5); ``` 以上例子是無向圖,如果是有向圖的話,可以自己想想看 ## 儲存帶權有向圖 有時候,我們要儲存的對象可能不是單純的有向 / 無向圖,像是若要計算兩點最短距離,那可能會在邊上賦值。圖上附加其他屬性的例子百百種,所以我們很常需要配合其他的資料結構來儲存圖的附加屬性 ### 點權 由於在電腦中,節點都是有編號的,因此一個一維陣列就足夠了 其他像是度數等性質也都是這樣完成儲存 ### 邊權 通常邊權在數學上的定義是 $f:E\rightarrow \mathbb{R}$,但是在競程通常只會碰整數,所以是 $f:E\rightarrow \mathbb{Z}$,但是這要如何儲存呢? 直觀上,從鄰接矩陣的定義來說,開一個二維陣列就可以解決,但是往往我們都是用鄰接串列來存才能避免用到太大的空間。因此我們只需要在鄰接串列上附加一個空間專門來存邊權即可 在實作上我們可以用 pair 來解決,剩下都一樣 ```cpp typedef pair<int, int> pii; vector<pii> vec[N]; // vec[u] = {v, w}: u 為起點, v 為終點, w 為路權 ``` 但凡事必有例外,像是 Floyd-Warshall 演算法就必須使用二維陣列 (矩陣) 來實作,而非鄰接串列 ## 邊陣列 Edge Array 不難發現其實一條邊其實就兩個終點而已,頂多再加上邊權,因此可以考慮直接開一個 pair / struct 陣列把所有邊的資訊儲存起來。要注意的是這種儲存方法並不適合遍歷。在 [Bellman-Ford 演算法](https://hackmd.io/@ShanC/Bellman-Ford-Algorithm)時會用到這種儲存方法 ### 無權邊 ```cpp= pair<int, int> edges[MXN]; // 這裡儲存邊的兩個終點 for(int i = 0; i < m; i++){ // 輸入可以這樣處理 cin >> edges[i].first >> edges[i].second; } ``` ### 帶權邊 ```cpp= struct Edge{ // 儲存兩終點 u, v 與邊的權重 w int u, v, w; }edge[MXN]; for(int i = 0; i < m; i++){ // 輸入可以這樣處理 cin >> edge[i].u >> edge[i].v >> edge[i].w; } ``` ## 題目練習 [Atcoder abc_232 C - Graph Isomorphism](https://atcoder.jp/contests/abc232/tasks/abc232_c) (圖同構裸題) ---- ## 參考資料 - Introduction to Algorithms, Fourth Edition - D.B.West - Introduction to Graph Theory 2/e ---- > 上一篇: [基礎圖論 (三)](https://hackmd.io/@ShanC/basic-graph-theory-3) > [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC) > 作者: ShanC > 更新: 2025/8/1