第8章 圖
數據結構與算法_師大完整教程目錄(更有python、go、pytorch、tensorflow、爬蟲、人工智能教學等着你):https://www.cnblogs.com/nickchen121/p/13768298.html
一、圖的基本概念
-
圖 \(G\) 的頂點集:記作 \(V(G)\)
-
圖 \(G\) 的邊集:記作 \(E(G)\)
-
無向圖的邊:頂點 \(v\) 到 頂點 \(u\) 的邊記作 \((u,v)\)
-
有向圖的邊:頂點 \(v\) 到 頂點 \(u\) 的邊記作 \(<u,v>\)
-
鄰接點:若 \((v,u)\) 是一條無向邊,則稱 \(u\) 和 \(v\) 互為鄰接點
-
頂點和邊數的關系:
- \(n\) 個頂點的無向圖,其邊數 \(e\) 小於等於 \(n(n-1)/2\)。邊數恰好為 \(n(n-1)/2\) 的無向圖稱為無向完全圖
- \(n\) 個頂點的有向圖,其邊數 \(e\) 小於等於 \(n(n-1)\)。邊數恰好為 \(n(n-1\) 的有向圖稱為有向完全圖
-
無向圖頂點的度:頂點相關聯的邊的數目,頂點 \(v\) 的度記為 \(D(v)\)
-
有向圖頂點的度:
- 入度:以頂點 \(v\) 作為終點的邊的數目,記為 \(ID(v)\)
- 出度:以頂點 \(v\) 作為始點的邊的數目,記為 \(OD(v)\)
- 注:有向圖頂點的度等於頂點的入度和出度之和
-
頂點數 \(n\)、邊數 \(e\) 和度數的關系:\(e=\frac{1}{2}\sum_{i=1}^n{D(v_i)}\)
-
有向圖路徑:存在一個頂點序列 \(v,v_1,v_2,\cdots,v_m,u\),且 \((v,v_1),(v_1,v_2),\cdots,(v_m,u)\) 都屬於 \(E(G)\),則該頂點序列為 \(v\) 到 \(u\) 的一條路徑
-
無向圖路徑:有向圖路徑:存在一個頂點序列 \(v,v_1,v_2,\cdots,v_m,u\),且 \(<v,v_1>,<v_1,v_2>,\cdots,<v_m,u>\) 都屬於 \(E(G)\),則該頂點序列為 \(v\) 到 \(u\) 的一條路徑
-
簡單路徑:一條路徑除了起點 \(v\) 和終點 \(u\) 之外,其余頂點均不相同,該路徑稱之為一條簡單路徑
-
簡單回路(簡單環):起點和終點相同(\(v=u\))的簡單路徑
-
無向圖連通的概念:
- 無向圖的連通:頂點 \(v\) 到 \(u\) 之間有路徑,則稱 \(v\) 和 \(u\) 是連通的
- 連通圖(無向圖):\(V(G)\) 中任意兩個不同的頂點 \(v\) 和 \(u\) 都連通(即有路徑),則 \(G\) 為連通圖
- 連通分量:無向圖 \(G\) 的極大連通子圖(任何連通圖的連通分量都是其本身,非連通的無向圖有多個連通分量)
-
有向圖連通的概念:
- 強連通圖:\(V(G)\) 中任意兩個不同的頂點 \(v\) 和 \(u\),都存在從 \(v\) 到 \(u\) 和從 \(u\) 到 \(v\) 的路徑
- 強連通分量:有向圖的極大強連通子圖(任何強連通圖的唯一強連通分量是其自身,非強連通的有向圖有多個強連通分量)
-
連通分量和強連通分量注意事項:單個頂點可以屬於一個單獨的連通分量或強連通分量
-
權:圖的每條邊上附上相關的數值
-
網絡(帶權圖):圖的每條邊都賦上一個權
二、圖的基本運算
- 略
三、圖的基本存儲結構
- 注:對於以下圖的存儲結構,都假定頂點序號從 \(0\) 開始,圖 \(G\) 的頂點集一般形式為 \(V(G)=\{v_0,\cdots,v_i,\cdots,v_{n-1}\}\)
3.1 鄰接矩陣及其實現
-
圖的鄰接矩陣表示法:用兩個表格分別存儲數據元素(頂點)的信息和數據之間的關聯(邊)信息
- 一維數組(順序表):存儲數據元素的信息
- 二維數組(鄰接矩陣):存儲數據元素之間的關系
-
鄰接矩陣的性質:若 \((v_i,v_j)或<v_i,v_j>\in{E(G)}\),\(A[i,j]=1\);若 \((v_i,v_j)or<v_i,v_j>\notin{E(G)}\),\(A[i,j]=0\)
-
無向圖的鄰接矩陣:該鄰接矩陣一定是對稱的,可以采用上(下)三角矩陣進行壓縮存儲,存儲空間為 \(n(n+1)/2\)
-
有向圖的鄰接矩陣:該鄰接矩陣不一定是對稱的,存儲空間為 \(n^2\)
-
無向圖頂點 \(v_i\) 的度數:\(D(v_i) = \sum_{j=0}^{n-1}A[i,j]=\sum_{j=0}^{n-1}A[j,i]\) (對稱矩陣\(A=A^T\))
-
有向圖頂點 \(v_i\) 的度數:
- 出度:\(OD(v_i)=\sum_{j=0}^{n-1}A[i,j]\)
- 入度:\(ID(v_i) = \sum_{j=0}^{n-1}A[j,i]\)
-
圖鄰接矩陣圖:

-
網絡的鄰接矩陣性質:
- 圖網絡鄰接矩陣圖:

3.1.1 鄰接矩陣存儲結構
#define FINITY 5000 // 用5000表示無窮大
#define M 20 // 最大頂點數
typedef char vertextype; // 頂點值類型
typedef int edgetype; // 權值類型
typedef struct {
vertextype vexs[M]; // 頂點信息域
edgetype edges[M][M]; // 鄰接矩陣
int n, e; // 圖中頂點總數和邊數
} Mgraph;
3.1.2 建立網絡的鄰接矩陣算法(算法)
-
算法步驟:
- 打開文件
- 讀入圖中的頂點數和邊數
- 讀入圖中的頂點值
- 初始化鄰接矩陣
- 讀入網絡中的邊
- 建立無向圖鄰接矩陣
- 關閉文件
3.2 鄰接表及其實現
-
鄰接表與鄰接矩陣的區別:
- 頂點個數為 \(n\) 的圖,鄰接矩陣的存儲空間為 \(n^2\) ,而使用鄰接表則可節省很多空間
- 鄰接矩陣表示法唯一,而鄰接表的表示法不唯一
-
鄰接表中的兩個結點:
- 表頭結點:存儲頂點的數據域(\(vertex\))和頭指針域(\(firstedge\))
- 邊表結點:鄰接點域(\(adjvex\))和鏈域(\(next\))
-
鄰接表的隨機訪問任意頂點的構造:讓所有頭結點順序存儲在一個向量中
-
圖無向圖鄰接表:

-
有向圖
- 出邊表(鄰接表):表結點存儲以頂點 \(v\) 為始點射出的一條邊
- 入邊表(逆鄰接表):表結點存儲以頂點 \(v\) 為終點射出的一條邊
-
圖有向圖的鄰接表:

-
鄰接表存儲空間:無向圖(\(n\) 個頂點和 \(e\) 條邊)用鄰接表存儲需要 \(n\) 個頭結點和 \(2e\) 個邊結點
-
注:當 \(e\) 遠小於 \(n(n-1)/2\) 時,鄰接表存儲圖比鄰接矩陣存儲圖節省空間
-
無向圖的度(鄰接表):頂點 \(v_i\) 的度為第 \(i\) 個鏈表中結點的個數
-
有向圖的度(鄰接表-出邊表):
- 出度:第 \(i\) 個鏈表中的結點的個數
- 入度:所有鏈表中其鄰接點域的值為 \(i\) 的結點的個數
3.2.1 鄰接表存儲結構
#define M 20 // 預定義圖的最大頂點數
typedef char datatype; // 定點信息數據域
// 邊表結點類型
typedef struct node {
int adjvex; // 鄰接點
struct node *next;
} edgenode;
// 頭結點類型
typedef struct vnode {
datatype vertex; // 頂點信息
edgenode *firstedge; // 鄰接鏈表頭指針
} vertexnode;
// 鄰接表類型
typedef struct {
vertexnode adjlist[M]; // 存放頭結點的順序表
int n, e; // 圖的頂點數與邊數
} linkedgraph;
3.2.2 建立無向圖的鄰接表算法(算法)
-
算法步驟:
- 打開文件
- 讀入頂點數和邊數
- 讀入頂點信息
- 邊表置為空
- 循環 e(邊數) 次建立邊表
輸入無序對 \((i,j)\)
鄰接點序號為 \(j\)
將新結點 \(*s\) 插入頂點 \(v_i\) 的邊表頭部
9. 關閉文件
3.3 鄰接多重表
-
鄰接多重表由兩個表組成:
- 表頭結點:\(vertex\) 域存儲頂點的相關信息;\(firstedge\) 存儲第一條關聯於 \(vertex\) 頂點的邊
- 邊表結點:\(mark\) 域標志圖的遍歷算法中是否被搜索過;\(vex_i\) 和 \(vex_j\) 表示邊的兩個頂點在圖中的位序;\(link_i\) 和 \(link_j\) 表示兩個邊表結點指針。
\(link_i\) 指向關聯於頂點 \(vex_i\)的下一條邊;\(link_j\) 指向關聯於頂點 \(vex_j\) 的下一條邊
4. 邊表結點的順序如下表所示:
| \(mark\) | \(vex_i\) | \(link_i\) | \(vex_j\) | \(link_j\) |
|---|---|---|---|---|
四、圖的遍歷
4.1 深度優先遍歷(DFS)
-
深度優先遍歷步驟:
- 選取一個源點 \(v\in{V}\),把 \(v\) 標記為已被訪問
- 使用深度優先搜索方法依次搜索 \(v\) 的所有鄰接點 \(w\)
- 如果 \(w\) 未被訪問,以 \(w\) 為新的出發點繼續進行深度優先遍歷
- 如果從 \(v\) 出發,有路的頂點都被訪問過,則從 \(v\) 的搜索過程結束
- 如果圖中還有未被訪問過的頂點(該圖有多個連通分量或者強連通分量),則再任選一個未被訪問的頂點開始新的搜索
-
注:深度優先遍歷的順序是不一定的,但是,當采用鄰接表存儲結構並且存儲結構已確定的情況下,遍歷的結果將是確定的
-
圖深度優先遍歷:

4.2 廣度優先遍歷 (BFS)
-
廣度優先遍歷步驟:
- 從圖中某個源點 \(v\) 出發
- 訪問頂點 \(v\) 后,盡可能先橫向搜索 \(v\) 的所有鄰接點
- 在訪問 \(v\) 的各個鄰接點 \(w_k,\cdots,w_k\) 后,從這些鄰接點出發依次訪問與 \(w_1,\cdots,w_k\) 鄰接的所有未曾訪問過的頂點
- 如果 \(G\) 是連通圖,訪問完成;否則另選一個尚未訪問的頂點作為新源點繼續上述搜索過程,知道圖 \(G\) 的所有頂點均被訪問
-
注:廣度優先遍歷無向圖的過程中,調用 \(bfs(函數:從頂點\,i\,出發廣度優先遍歷圖\,g\,的連通分量)\) 的次數就是該圖連通分量的個數
-
圖廣度優先遍歷:

五、生成樹與最小生成樹 (無向網)
- 生成樹:對於一個無向的連通圖 \(G\),設 \(G'\) 是它的一個子圖,如果 \(G'\) 中包含了 \(G\) 中所有的頂點,且 \(G'\) 是無回路的連通圖,則稱 \(G'\) 為 \(G\) 的一顆生成樹
- 圖生成樹:

- 生成森林:如果 \(G\) 是非連通的無向圖,需要若干次從外部調用 \(DFS(或BFS)\) 算法才能完成對 \(G\) 的遍歷。每一次外部調用,只能訪問 \(G\) 的一個連通分量,經過該連通分量的頂點和邊構造出一顆生成樹,則 \(G\) 的各個連通分量的生成樹組成了 \(G\) 的生成森林
- 圖生成森林:

5.1 最小生成樹的定義
-
生成樹的權:生成樹 \(T\) 的各邊的權值總和,記作 \(W(T)=\sum_{(u,v)\in{E}}w_{uv}\),其中 \(E\) 表示 \(T\) 的邊集,\(w_{uv}\) 表示邊 \((u,v)\) 的權
-
最小生成樹的權:總權值 \(W(T)\) 最小的生成樹稱為最小生成樹
-
構造最小生成樹的准則:
- 必須只使用該網絡中的邊來構造最小生成樹
- 必須使用且僅使用 \(n-1\) 條邊來連接網絡中的 \(n\) 個頂點
- 不能使用產生回路的邊
5.2 最小生成樹的普里姆(Prim)算法
-
兩棲邊:假設 \(G=(V,E)\) 是一個連通圖,\(U\) 是頂點集 \(V\) 的一個非空真子集,若 \((u,v)\) 是滿足 \(u\in{U},v\in{V-U}\) 的邊(稱這個邊為兩棲邊),且 \((u,v)\) 在所有的兩棲邊中具有最小的權值(此時,稱 \((u,v)\) 為最小兩棲邊)
-
Prim算法步驟:
- 初始化 \(U=\{u_0\},TREE=\{\}\)
- 如果 \(U=V(G)\),則輸出最小生成樹 \(T\),並結束算法
- 在所有兩棲邊中找一條權最小的邊 \((u,v)\)(若候選兩棲邊中的最小邊不止一條,可任選其中的一條),將邊 \((u,v)\) 加入到邊集 \(TREE\) 中,並將頂點 \(v\) 並入集合 \(U\) 中
- 由於新頂點的加入,\(U\) 的狀態發生變化,需要對 \(U\) 與 \(V-U\) 之間的兩棲邊進行調整
- 轉步驟 \(2\)
-
下圖步驟順序(\(V=\{A,B,C,D,E,F\}\)):
- \(U = \{A\}\),\(V-U=\{B,C,D,E,F\}\),\(TREE=\{\}\),兩棲邊 \((A,B),(A,C),(A,D),(A,E),(A,F)\),最小兩棲邊 \((A,B)\)
- \(U = \{A,B\}\),\(V-U=\{C,D,E,F\}\),\(TREE=\{(A,B)\}\),兩棲邊 \((B,C),(B,D),(B,F),(A,E)\)(\((B,C)\) 比 (\(A,C)\) 小,因此做一個替換),最小兩棲邊 \((B,D)\)
- \(U = \{A,B,D\}\),\(V-U=\{C,E,F\}\),\(TREE=\{(A,B),(B,D)\}\),兩棲邊 \((B,C),(B,F),(A,E)\),最小兩棲邊 \((B,F)\)
- \(U = \{A,B,D,F\}\),\(V-U=\{C,E\}\),\(TREE=\{(A,B),(B,D),(B,F)\}\),兩棲邊 \((B,C),((F,E)\),最小兩棲邊 \((B,C)\)
- \(U = \{A,B,D,F,C\}\),\(V-U=\{E\}\),\(TREE=\{(A,B),(B,D),(B,F),(B,C)\}\),兩棲邊 \((F,E)\),最小兩棲邊 \((F,E)\)
- \(U = \{A,B,D,F,C,E\}\),\(V-U=\{\}\),\(TREE=\{(A,B),(B,D),(B,F),(B,C),(F,E)\}\),兩棲邊 \(\{\}\),最小兩棲邊 \(\{\}\)
-
圖prim算法:

5.3 最小生成樹的克魯斯卡爾(Kruskal)算法
-
算法步驟:
- 將圖 \(G\) 看做一個森林,每個頂點為一棵獨立的樹
- 將所有的邊加入集合 \(S\),即一開始 \(S = E\)
- 從 \(S\) 中拿出一條最短的邊 \((u,v)\),如果 \((u,v)\) 不在同一棵樹內,則連接 \(u,v\) 合並這兩棵樹,同時將 \((u,v)\) 加入生成樹的邊集 \(E'\)
- 重復步驟 \(3\) 直到所有點屬於同一棵樹,邊集 \(E'\) 就是一棵最小生成樹
-
圖kruskal算法:

六、最短路徑 (有向網)
6.1 單源最短路徑(Dijkstra算法)
-
距離向量 \(d\):表示源點可以途徑 \(S\) 中的頂點到達 \(V-S\) 中頂點的距離
-
路徑向量 \(p\):保存路徑上第 \(i\) 個頂點的前驅頂點序號(沒有的話,默認為 \(-1\))
-
算法步驟:
- 找到與源點 \(v\) 最近的頂點,並將該頂點並入最終集合 \(S\)
- 根據找到的最近的頂點更新從源點 \(v\) 出發到集合 \(V-S\) 上可達頂點的最短路徑
- 重復以上操作
-
圖Dijkstra算法:

-
上圖求單源最短路徑步驟:
- 初始化:從源點 \(v1\) 出發得到矩陣,到達個點的最小路徑是 \(D_{12}=10\),\(D_{0}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &\infty &30 &100\\ \end{array}\right ]\)
- 第一次:從 \(v2\) 點出發,\(v1\) 和 \(v2\) 保持不變,迭代剩下點 \((v3,v4,v5)\) 的距離后,剩余點的最短路徑是 \(v4\),\(D_{1}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &60(10+50) &30 &100\\ \end{array}\right ]\)
- 第二次:從 \(v4\) 點 出發,\(v1,v2,v4\) 保持不變,優化剩余點 \((v3,v5)\) 的最短距離。剩余點的最短路徑是 \(v3\),\(D_{2}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\\ 0 &10 &50(20+30) &30 &90(30+60)\\ \end{array}\right ]\)
- 第三次:從 \(v3\) 點出發,\(v1,v2,v4,v3\) 保持不變。優化剩余點 $v5 \(的最短路徑,\)D_{3}=\left[\begin{array}{cccc} v1 &v2 &v3 &v4 &v5\ 0 &10 &50 &30 &60(20+30+10)\ \end{array}\right ]$
-
源點 \(v1\) 的 \(Dijkstra算法\) 的最短路徑(如下表):
- | 中間頂點 | 終點 | 路徑長度 |
| :--: | :--: | :--: |
| | 2 | 10 |
| 4 | 3 | 50 |
| | 4 | 30 |
| 4;3 | 5 | 60 |
- | 中間頂點 | 終點 | 路徑長度 |
-
距離向量 \(d\) 和路徑向量 \(p\):
-
圖Dijkstra算法的輔助數組:

6.2 所有頂點對的最短路徑 (Floyd算法)
- 算法步驟:略
七、拓撲排序 (AOV網)
-
\(AOV網\) 邊的作用:表示活動間(一個頂點表示一個活動)的優先關系
-
注:(真題)拓撲排序可以判斷圖中有沒有回路(深度遍歷優先算法也可以)
-
算法步驟
- 在有向圖中找一個沒有前驅(入度為 \(0\))的頂點並輸出
- 在圖中刪除該頂點以及所有從該頂點發出的有向邊
- 反復執行步驟 \(1\) 和 \(2\),知道所有頂點均被輸出,或者 \(AOV\) 網中再也沒有入度為 \(0\) 的頂點存在為止
-
圖拓撲排序:

八、關鍵路徑 (AOE網)
-
\(AOE網\) 邊的作用:表示活動(一個頂點表示一個活動)持續的時間
-
事件可能的最早開始時間 \(v_e(i)\):對於某一事件 \(v_i\),它可能的最早發生時間 \(v_e(i)\) 是從源點到頂點 \(v_i\) 的最大路徑長度
-
\[\]
-
\begin{cases}v_l(n-1)=v_e(n-1)\v_l=min{v_l(j)-len(<v_i,v_j>)}(0\leq{i}\leq{n-2})\end{cases}
\begin{cases}e(k)=v_e(i)\l(k)=v_l(j)-len(<v_i,v_j>)\end{cases}

