C#圖表算法之無向圖
圖是由一組頂點(diǎn)和一組能夠?qū)蓚€(gè)頂點(diǎn)相連的邊組成。
頂點(diǎn)叫什么名字并不重要,但我們需要一個(gè)方法來指代這些頂點(diǎn)。一般使用 0 至 V-1 來表示一張含有 V 個(gè)頂點(diǎn)的圖中的各個(gè)頂點(diǎn)。這樣約定是為了方便使用數(shù)組的索引來編寫能夠高效訪問各個(gè)頂點(diǎn)信息的代碼。用一張符號(hào)表來為頂點(diǎn)的名字和 0 到 V-1 的整數(shù)值建立一一對(duì)應(yīng)的關(guān)系并不困難,因此直接使用數(shù)組索引作為結(jié)點(diǎn)的名稱更方便且不失一般性,也不會(huì)損失什么效率。
我們用 v-w 的記法來表示連接 v 和 w 的邊, w-v 是這條邊的另一種表示方法。
在繪制一幅圖時(shí),用圓圈表示頂點(diǎn),用連接兩個(gè)頂點(diǎn)的線段表示邊,這樣就能直觀地看出圖地結(jié)構(gòu)。但這種直覺有時(shí)可能會(huì)誤導(dǎo)我們,因?yàn)閳D地定義和繪制地圖像是無關(guān)的,一組數(shù)據(jù)可以繪制不同形態(tài)的圖像。
特殊的圖
自環(huán):即一條連接一個(gè)頂點(diǎn)和其自身的邊;
多重圖:連接同一對(duì)頂點(diǎn)的兩條邊成為平行邊,含有平行邊的圖稱為多重圖。
沒有平行邊的圖稱為簡(jiǎn)單圖。
1.相關(guān)術(shù)語
當(dāng)兩個(gè)頂點(diǎn)通過一條邊相連時(shí),稱這兩個(gè)頂點(diǎn)是相鄰得,并稱這條邊依附于這兩個(gè)頂點(diǎn)。某個(gè)頂點(diǎn)的度數(shù)即為依附于它的邊的總數(shù)。子圖是由一幅圖的所有邊的一個(gè)子集(以及它們所依附的所有頂點(diǎn))組成的圖。許多計(jì)算問題都需要識(shí)別各種類型的子圖,特別是由能夠順序連接一系列頂點(diǎn)的邊所組成的子圖。
在圖中,路徑是由邊順序連接的一系列頂點(diǎn)。簡(jiǎn)單路徑是一條沒有重復(fù)頂點(diǎn)的路徑。環(huán)是一條至少含有一條邊且起點(diǎn)和終點(diǎn)相同的路徑。簡(jiǎn)單環(huán)是一條(除了起點(diǎn)和終點(diǎn)必須相同之外)不含有重復(fù)頂點(diǎn)和邊的環(huán)。路徑或環(huán)的長(zhǎng)度為其中所包含的邊數(shù)。
當(dāng)兩個(gè)頂點(diǎn)之間存在一條連接雙方的路徑時(shí),我們稱一個(gè)頂點(diǎn)和另一個(gè)頂點(diǎn)是連通的。
如果從任意一個(gè)頂點(diǎn)都存在一條路徑到達(dá)另一個(gè)任意頂點(diǎn),我們稱這副圖是連通圖。一幅非連通的圖由若干連通的部分組成,它們都是其極大連通子圖。
一般來說,要處理一張圖需要一個(gè)個(gè)地處理它的連通分量(子圖)。
樹是一幅無環(huán)連通圖?;ゲ幌噙B的樹組成的集合稱為森林。連通圖的生成樹是它的一幅子圖,它含有圖中的所有頂點(diǎn)且是一棵樹。圖的生成森林是它的所有連通子圖的生成樹的集合。
樹的定義非常通用,稍作改動(dòng)就可以變成用來描述程序行為(函數(shù)調(diào)用層次)模型和數(shù)據(jù)結(jié)構(gòu)。當(dāng)且僅當(dāng)一幅含有 V 個(gè)結(jié)點(diǎn)的圖 G 滿足下列 5 個(gè)條件之一時(shí),它就是一棵樹:
- G 有 V - 1 條邊且不含有環(huán);
- G 有 V - 1 條邊且是連通的;
- G 是連通的,但刪除任意一條都會(huì)使它不再連通;
- G 是無環(huán)圖,但添加任意一條邊都會(huì)產(chǎn)生一條環(huán);
- G 中的任意一對(duì)頂點(diǎn)之間僅存在一條簡(jiǎn)單路徑;
圖的密度是指已經(jīng)連接的頂點(diǎn)對(duì)占所有可能被連接的頂點(diǎn)對(duì)的比例。在稀疏圖中,被連接的頂點(diǎn)對(duì)很少;而在稠密圖中,只有少部分頂點(diǎn)對(duì)之間沒有邊連接。一般來說,如果一幅圖中不同的邊的數(shù)量在頂點(diǎn)總數(shù) v 的一個(gè)小的常數(shù)倍以內(nèi),那么我們認(rèn)為這幅圖是稀疏的,否則就是稠密的。
二分圖是一種能夠?qū)⑺薪Y(jié)點(diǎn)分為兩部分的圖,其中圖的每條邊所連接的兩個(gè)頂點(diǎn)都分別屬于不同的集合。
2.表示無向圖的數(shù)據(jù)結(jié)構(gòu)
圖的幾種表示方法
接下來要面對(duì)的圖處理問題就是用哪種數(shù)據(jù)結(jié)構(gòu)來表示圖并實(shí)現(xiàn)這份API,包含下面兩個(gè)要求:
- 1.必須為可能在應(yīng)用中碰到的各種類型圖預(yù)留出足夠的空間;
- 2.Graph 的實(shí)例方法的實(shí)現(xiàn)一定要快。
下面有三種選擇:
- 1.鄰接矩陣:我們可以使用一個(gè) V 乘 V 的布爾矩陣。當(dāng)頂點(diǎn) v 和 w 之間有連接的邊時(shí),定義 v 行 w 列的元素值為 true,否則為 false。這種表示方法不符合第一個(gè)條件--含有上百萬個(gè)頂點(diǎn)的圖所需的空間是不能滿足的。
- 2.邊的數(shù)組:我們可以使用一個(gè) Edge 類,它含有兩個(gè) int 實(shí)例變量。這種表示方法很簡(jiǎn)單但不滿足第二個(gè)條件--要實(shí)現(xiàn) Adj 需要檢查圖中的所有邊。
- 3.鄰接表數(shù)組:使用一個(gè)以頂點(diǎn)為索引的列表數(shù)組,其中每個(gè)元素都是和該頂點(diǎn)相連的頂點(diǎn)列表。
非稠密圖的標(biāo)準(zhǔn)表示成為鄰接表的數(shù)據(jù)結(jié)構(gòu),它將每個(gè)頂點(diǎn)的所有相鄰頂點(diǎn)都保存在該頂點(diǎn)對(duì)應(yīng)的元素所指向的一張鏈表中。我們使用這個(gè)數(shù)組就是為了快速訪問給定頂點(diǎn)的鄰接頂點(diǎn)列表。這里使用 Bag 來實(shí)現(xiàn)這個(gè)鏈表,這樣我們就可以在常數(shù)時(shí)間內(nèi)添加新的邊或遍歷任意頂點(diǎn)的所有相鄰頂點(diǎn)。
要添加一條連接 v 與 w 的邊,我們將 w 添加到 v 的鄰接表中并把 v 添加到 w 的鄰接表中。因此在這個(gè)數(shù)據(jù)結(jié)構(gòu)中每條邊都會(huì)出現(xiàn)兩次。這種 Graph 的實(shí)現(xiàn)的性能特點(diǎn):
- 1.使用的空間和 V+E 成正比;
- 2.添加一條邊所需的時(shí)間為常數(shù);
- 3.遍歷頂點(diǎn) v 的所有相鄰頂點(diǎn)所需的時(shí)間和 v 的度數(shù)成正比。
對(duì)于這些操作,這樣的特性已經(jīng)是最優(yōu)的了,而且支持平行邊和自環(huán)。注意,邊的插入順序決定了Graph 的鄰接表中頂點(diǎn)的出現(xiàn)順序。多個(gè)不同的鄰接表可能表示著同一幅圖。因?yàn)樗惴ㄔ谑褂?Adj() 處理所有相鄰的頂點(diǎn)時(shí)不會(huì)考慮它們?cè)卩徑颖碇械某霈F(xiàn)順序,這種差異不會(huì)影響算法的正確性,但在調(diào)試或是跟蹤?quán)徑颖淼能壽E時(shí)需要注意這一點(diǎn)。
public class Graph { private int v; private int e; private List<int>[] adj; //鄰接表(用List 代替 bag) /// <summary> /// 創(chuàng)建一個(gè)含有V個(gè)頂點(diǎn)但不含有邊的圖 /// </summary> /// <param name="V"></param> public Graph(int V) { v = V; e = 0; adj = new List<int>[V]; for (var i = 0; i < V; i++) adj[i] = new List<int>(); } public Graph(string[] strs) { foreach (var str in strs) { var data = str.Split(' '); int v = Convert.ToInt32(data[0]); int w = Convert.ToInt32(data[1]); AddEdge(v,w); } } /// <summary> /// 頂點(diǎn)數(shù) /// </summary> /// <returns></returns> public int V() { return v; } /// <summary> /// 邊數(shù) /// </summary> /// <returns></returns> public int E() { return e; } /// <summary> /// 向圖中添加一條邊 v-w /// </summary> /// <param name="v"></param> /// <param name="w"></param> public void AddEdge(int v, int w) { adj[v].Add(w); adj[w].Add(v); e++; } /// <summary> /// 和v相鄰的所有頂點(diǎn) /// </summary> /// <param name="v"></param> /// <returns></returns> public IEnumerable<int> Adj(int v) { return adj[v]; } /// <summary> /// 計(jì)算 V 的度數(shù) /// </summary> /// <param name="G"></param> /// <param name="V"></param> /// <returns></returns> public static int Degree(Graph G, int V) { int degree = 0; foreach (int w in G.Adj(V)) degree++; return degree; } /// <summary> /// 計(jì)算所有頂點(diǎn)的最大度數(shù) /// </summary> /// <param name="G"></param> /// <returns></returns> public static int MaxDegree(Graph G) { int max = 0; for (int v = 0; v < G.V(); v++) { var d = Degree(G, v); if (d > max) max = d; } return max; } /// <summary> /// 計(jì)算所有頂點(diǎn)的平均度數(shù) /// </summary> /// <param name="G"></param> /// <returns></returns> public static double AvgDegree(Graph G) { return 2.0 * G.E() / G.V(); } /// <summary> /// 計(jì)算自環(huán)的個(gè)數(shù) /// </summary> /// <param name="G"></param> /// <returns></returns> public static int NumberOfSelfLoops(Graph G) { int count = 0; for (int v = 0; v < G.V(); v++) { foreach (int w in G.Adj(v)) { if (v == w) count++; } } return count / 2; //每條邊都被計(jì)算了兩次 } public override string ToString() { string s = V() + " vertices, " + E() + " edges\n"; for (int v = 0; v < V(); v++) { s += v + ":"; foreach (int w in Adj(v)) { s += w + " "; } s += "\n"; } return s; } }
在實(shí)際應(yīng)用中還有一些操作可能有用,例如:
- 添加一個(gè)頂點(diǎn);
- 刪除一個(gè)頂點(diǎn)。
實(shí)現(xiàn)這些操作的一種方法是,使用符號(hào)表 ST 來代替由頂點(diǎn)索引構(gòu)成的數(shù)組,這樣修改之后就不需要約定頂點(diǎn)名必須是整數(shù)了??赡苓€需要:
- 刪除一條邊;
- 檢查圖是否含有 v-w。
要實(shí)現(xiàn)這些方法,可能需要使用 SET 代替 Bag 來實(shí)現(xiàn)鄰接表。我們稱這種方法為鄰接集?,F(xiàn)在還不需要,因?yàn)椋?/p>
- 不需要添加,刪除頂點(diǎn)和邊或是檢查一條邊是否存在;
- 上述操作使用頻率很低或者相關(guān)鏈表很短,可以直接使用窮舉法遍歷;
- 某些情況下會(huì)使性能損失 logV。
3.圖的處理算法的設(shè)計(jì)模式
因?yàn)槲覀儠?huì)討論大量關(guān)于圖處理的算法,所以設(shè)計(jì)的首要目標(biāo)是將圖的表示和實(shí)現(xiàn)分離開來。為此,我們會(huì)為每個(gè)任務(wù)創(chuàng)建一個(gè)相應(yīng)的類,用例可以創(chuàng)建相應(yīng)的對(duì)象來完成任務(wù)。類的構(gòu)造函數(shù)一般會(huì)在預(yù)處理中構(gòu)造各種數(shù)據(jù)結(jié)構(gòu),以有效地響應(yīng)用例的請(qǐng)求。典型的用例程序會(huì)構(gòu)造一幅圖,將圖作為參數(shù)傳遞給某個(gè)算法類的構(gòu)造函數(shù),然后調(diào)用各種方法來獲取圖的各種性質(zhì)。
我們用起點(diǎn) s 區(qū)分作為參數(shù)傳遞給構(gòu)造函數(shù)的頂點(diǎn)與圖中的其他頂點(diǎn)。在這份 API 中,構(gòu)造函數(shù)的任務(wù)就是找到圖中與起點(diǎn)連通的其他頂點(diǎn)。用例可以調(diào)用 marked 方法和 count 方法來了解圖的性質(zhì)。方法名 marked 指的是這種基本方法使用的一種實(shí)現(xiàn)方式:在圖中從起點(diǎn)開始沿著路徑到達(dá)其他頂點(diǎn)并標(biāo)記每個(gè)路過的頂點(diǎn)。
在 union-find算法已經(jīng)見過 Search API 的實(shí)現(xiàn),它的構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè) UF 對(duì)象,對(duì)圖中的每條邊進(jìn)行一次 union 操作并調(diào)用 connected(s,v) 來實(shí)現(xiàn) marked 方法。實(shí)現(xiàn) count 方法需要一個(gè)加權(quán)的 UF 實(shí)現(xiàn)并擴(kuò)展它的API,以便使用 count 方法返回 sz[find(v)]。
下面的一種搜索算法是基于深度優(yōu)先搜索(DFS)的,它會(huì)沿著圖的邊尋找喝起點(diǎn)連通的所有頂點(diǎn)。
4.深度優(yōu)先搜索
要搜索一幅圖,只需要一個(gè)遞歸方法來遍歷所有頂點(diǎn)。在訪問其中一個(gè)頂點(diǎn)時(shí):
- 1.將它標(biāo)記為已訪問;
- 2.遞歸地訪問它所有沒有被標(biāo)記過地鄰居頂點(diǎn)。
這種方法稱為深度優(yōu)先搜索(DFS)。
namespace Graphs { /// <summary> /// 使用一個(gè) bool 數(shù)組來記錄和起點(diǎn)連通地所有頂點(diǎn)。遞歸方法會(huì)標(biāo)記給定地頂點(diǎn)并調(diào)用自己來訪問該頂點(diǎn)地相鄰頂點(diǎn)列表中 /// 所有沒有被標(biāo)記過地頂點(diǎn)。 如果圖是連通的,每個(gè)鄰接鏈表中的元素都會(huì)被標(biāo)記。 /// </summary> public class DepthFirstSearch { private bool[] marked; private int count; public DepthFirstSearch(Graph G,int s) { marked = new bool[G.V()]; Dfs(G,s); } private void Dfs(Graph g, int V) { marked[V] = true; count++; foreach (var w in g.Adj(V)) { if (!marked[w]) Dfs(g,w); } } public bool Marked(int w) { return marked[w]; } } }
深度優(yōu)先搜索標(biāo)記與起點(diǎn)連通的所有頂點(diǎn)所需的時(shí)間和頂點(diǎn)的度數(shù)之和成正比。
這種簡(jiǎn)單的遞歸模式只是一個(gè)開始 -- 深度優(yōu)先搜索能夠有效處理許多和圖有關(guān)的任務(wù)。
- 1.連通性。給定一幅圖,兩個(gè)給定的頂點(diǎn)是否連通?(兩個(gè)給定的頂點(diǎn)之間是否存在一條路徑?路徑檢測(cè)) 圖中有多少個(gè)連通子圖?
- 2.單點(diǎn)路徑。給定一幅圖和一個(gè)起點(diǎn) s ,從 s 到給定目的頂點(diǎn) v 是否存在一條路徑?如果有,找出這條路徑。
5.尋找路徑
單點(diǎn)路徑的API:
構(gòu)造函數(shù)接受一個(gè)起點(diǎn) s 作為參數(shù),計(jì)算 s 到與 s 連通的每個(gè)頂點(diǎn)之間的路徑。在為起點(diǎn) s 創(chuàng)建 Paths 對(duì)象之后,用例可以調(diào)用 PathTo 方法來遍歷從 s 到任意和 s 連通的頂點(diǎn)的路徑上的所有頂點(diǎn)。
實(shí)現(xiàn)
下面的算法基于深度優(yōu)先搜索,它添加了一個(gè) edgeTo[ ] 整型數(shù)組,這個(gè)數(shù)組可以找到從每個(gè)與 s 連通的頂點(diǎn)回到 s 的路徑。它會(huì)記住每個(gè)頂點(diǎn)到起點(diǎn)的路徑,而不是記錄當(dāng)前頂點(diǎn)到起點(diǎn)的路徑。為了做到這一點(diǎn),在由邊 v-w 第一次任意訪問 w 時(shí),將edgeTo[w] = v 來記住這條路徑。換句話說, v-w 是從s 到 w 的路徑上最后一條已知的邊。這樣,搜索的結(jié)果是一棵以起點(diǎn)為根結(jié)點(diǎn)的樹,edgeTo[ ] 是一棵由父鏈接表示的樹。 PathTo 方法用變量 x 遍歷整棵樹,將遇到的所有頂點(diǎn)壓入棧中。
public class DepthFirstPaths { private bool[] marked; private int[] edgeTo; //從起點(diǎn)到一個(gè)頂點(diǎn)的已知路徑上的最后一個(gè)頂點(diǎn) private int s;//起點(diǎn) public DepthFirstPaths(Graph G, int s) { marked = new bool[G.V()]; edgeTo = new int[G.V()]; this.s = s; Dfs(G,s); } private void Dfs(Graph G, int v) { marked[v] = true; foreach (int w in G.Adj(v)) { if (!marked[w]) { edgeTo[w] = v; Dfs(G,w); } } } public bool HasPathTo(int v) { return marked[v]; } public IEnumerable<int> PathTo(int v) { if (!HasPathTo(v)) return null; Stack<int> path = new Stack<int>(); for (int x = v; x != s; x = edgeTo[x]) path.Push(x); path.Push(s); return path; } }
使用深度優(yōu)先搜索得到從給定起點(diǎn)到任意標(biāo)記頂點(diǎn)的路徑所需的時(shí)間與路徑長(zhǎng)度成正比。
6.廣度優(yōu)先搜索
深度優(yōu)先搜索得到的路徑不僅取決于圖的結(jié)構(gòu),還取決于圖的表示和遞歸調(diào)用的性質(zhì)。
單點(diǎn)最短路徑:給定一幅圖和一個(gè)起點(diǎn) s ,從 s 到給定目的頂點(diǎn) v 是否存在一條路徑?如果有,找出其中最短的那條(所含邊最少)。
解決這個(gè)問題的經(jīng)典方法叫做廣度優(yōu)先搜索(BFS)。深度優(yōu)先搜索在這個(gè)問題上沒有什么作用,因?yàn)樗闅v整個(gè)圖的順序和找出最短路徑的目標(biāo)沒有任何關(guān)系。相比之下,廣度又出現(xiàn)搜索正式為了這個(gè)目標(biāo)才出現(xiàn)的。
要找到從 s 到 v 的最短路徑,從 s 開始,在所有由一條邊就可以到達(dá)的頂點(diǎn)中尋找 v ,如果找不到就繼續(xù)在與 s 距離兩條邊的所有頂點(diǎn)中查找 v ,如此一直進(jìn)行。
在程序中,在搜索一幅圖時(shí)遇到有很多邊需要遍歷的情況時(shí),我們會(huì)選擇其中一條并將其他邊留到以后再繼續(xù)搜索。在深度優(yōu)先搜索中,我們用了一個(gè)可以下壓棧。使用LIFO (后進(jìn)先出)的規(guī)則來描述下壓棧和走迷宮時(shí)先探索相鄰的
通道類似。從有待搜索的通道中選擇最晚遇到過的那條。在廣度優(yōu)先搜索中,我們希望按照與起點(diǎn)距離的順序來遍歷所有頂點(diǎn),使用(FIFO,先進(jìn)先出)隊(duì)列來代替棧即可。我們將從有待搜索的通道中選擇最早遇到的那條。
實(shí)現(xiàn)
下面的算法使用了一個(gè)隊(duì)列來保存所有已經(jīng)被標(biāo)記過但其鄰接表還未被檢查過的頂點(diǎn)。先將頂點(diǎn)加入隊(duì)列,然后重復(fù)下面步驟知道隊(duì)列為空:
- 1.取隊(duì)列的下一個(gè)頂點(diǎn) v 并標(biāo)記它;
- 2.將與 v 相鄰的所有未被標(biāo)記過的頂點(diǎn)加入隊(duì)列。
下面的 Bfs 方法不是遞歸。它顯示地使用了一個(gè)隊(duì)列。和深度優(yōu)先搜索一樣,它的結(jié)果也是一個(gè)數(shù)組 edgeTo[ ] ,也是一棵用父鏈接表示的根結(jié)點(diǎn)為 s 的樹。它表示了 s 到每個(gè)與 s 連通的頂點(diǎn)的最短路徑。
namespace Graphs { /// <summary> /// 廣度優(yōu)先搜索 /// </summary> public class BreadthFirstPaths { private bool[] marked;//到達(dá)該頂點(diǎn)的最短路徑已知嗎? private int[] edgeTo;//到達(dá)該頂點(diǎn)的已知路徑上的最后一個(gè)頂點(diǎn) private int s;//起點(diǎn) public BreadthFirstPaths(Graph G,int s) { marked = new bool[G.V()]; edgeTo = new int[G.V()]; this.s = s; Bfs(G,s); } private void Bfs(Graph G, int s) { Queue<int> queue = new Queue<int>(); marked[s] = true;//標(biāo)記起點(diǎn) queue.Enqueue(s);//將它加入隊(duì)列 while (queue.Count > 0) { int v = queue.Dequeue();//從隊(duì)列中刪去下一個(gè)頂點(diǎn) foreach (var w in G.Adj(v)) { if (!marked[w])//對(duì)于每個(gè)未標(biāo)記的相鄰頂點(diǎn) { edgeTo[w] = v;//保存最短路徑的最后一條邊 marked[w] = true;//標(biāo)記它,因?yàn)樽疃搪窂揭阎? queue.Enqueue(w);//并將它添加到隊(duì)列中 } } } } public bool HasPathTo(int v) { return marked[v]; } } }
軌跡:
對(duì)于從 s 可達(dá)的任意頂點(diǎn) v ,廣度優(yōu)先搜索都能找到一條從 s 到 v 的最短路徑,沒有其他從 s 到 v 的路徑所含的邊比這條路徑更少。
廣度優(yōu)先搜索所需的時(shí)間在最壞情況下和 V+E 成正比。
我們也可以使用廣度優(yōu)先搜索來實(shí)現(xiàn)已經(jīng)用深度優(yōu)先搜索實(shí)現(xiàn)的 Search API,因?yàn)樗鼨z查所有與起點(diǎn)連通的頂點(diǎn)和邊的方法只取決于查找能力。
廣度優(yōu)先搜索和深度優(yōu)先搜索在搜索中都會(huì)先將起點(diǎn)存入數(shù)據(jù)結(jié)構(gòu),然后重復(fù)以下步驟直到數(shù)據(jù)結(jié)構(gòu)清空:
- 1.取其中的下一個(gè)頂點(diǎn)并標(biāo)記它;
- 2.將 v 的所有相鄰而又未被標(biāo)記的頂點(diǎn)加入數(shù)據(jù)結(jié)構(gòu)。
這兩個(gè)算法的不同之處在于從數(shù)據(jù)結(jié)構(gòu)中獲取下一個(gè)頂點(diǎn)的規(guī)則(對(duì)于廣度優(yōu)先搜索來說是最早加入的頂點(diǎn),對(duì)于深度優(yōu)先搜索來說是最晚加入的頂點(diǎn))。這種差異得到了處理圖的兩種完全不同的視角,盡管無論使用哪種規(guī)則,所有與起點(diǎn)連通的頂點(diǎn)和邊都會(huì)被檢查到。
深度優(yōu)先搜索不斷深入圖中并在棧中保存了所有分叉的頂點(diǎn);廣度優(yōu)先搜索則像扇面一般掃描圖,用一個(gè)隊(duì)列保存訪問過的最前端的頂點(diǎn)。深度優(yōu)先搜索探索一幅圖的方式是尋找離起點(diǎn)更遠(yuǎn)的頂點(diǎn),只在碰到死胡同時(shí)才訪問進(jìn)出的頂點(diǎn);廣度優(yōu)先搜索則首先覆蓋起點(diǎn)附近的頂點(diǎn),只在臨近的所有頂點(diǎn)都被訪問了之后才向前進(jìn)。根據(jù)應(yīng)用的不同,所需要的性質(zhì)也不同。
7.連通分量
深度優(yōu)先搜索的下一個(gè)直接應(yīng)用就是找出一幅圖的所有連通分量。在 union-find中 “與......連通” 是一種等價(jià)關(guān)系,它能夠?qū)⑺许旤c(diǎn)切分成等價(jià)類(連通分量)。
實(shí)現(xiàn)
CC 的實(shí)現(xiàn)使用了marked 數(shù)組來尋找一個(gè)頂點(diǎn)作為每個(gè)連通分量中深度優(yōu)先搜索的起點(diǎn)。遞歸的深度優(yōu)先搜索第一次調(diào)用的參數(shù)是頂點(diǎn) 0 -- 它會(huì)標(biāo)記所有與 0 連通的頂點(diǎn)。然后構(gòu)造函數(shù)中的 for 循環(huán)會(huì)查找每個(gè)沒有被標(biāo)記的頂點(diǎn)并遞歸調(diào)用 Dfs 來標(biāo)記和它相鄰的所有頂點(diǎn)。另外,還使用了一個(gè)以頂點(diǎn)作為索引的數(shù)組 id[ ] ,值為連通分量的標(biāo)識(shí)符,將同一連通分量中的頂點(diǎn)和連通分量的標(biāo)識(shí)符關(guān)聯(lián)起來。這個(gè)數(shù)組使得 Connected 方法的實(shí)現(xiàn)變得非常簡(jiǎn)單。
namespace Graphs { public class CC { private bool[] marked; private int[] id; private int count; public CC(Graph G) { marked = new bool[G.V()]; id = new int[G.V()]; for (var s = 0; s < G.V(); s++) { if (!marked[s]) { Dfs(G,s); count++; } } } private void Dfs(Graph G, int v) { marked[v] = true; id[v] = count; foreach (var w in G.Adj(v)) { if (!marked[w]) Dfs(G,w); } } public bool Connected(int v, int w) { return id[v] == id[w]; } public int Id(int v) { return id[v]; } public int Count() { return count; } } }
深度優(yōu)先搜索的預(yù)處理使用的時(shí)間和空間與 V + E 成正比且可以在常數(shù)時(shí)間內(nèi)處理關(guān)于圖的連通性查詢。由代碼可知每個(gè)鄰接表的元素都只會(huì)被檢查一次,共有 2E 個(gè)元素(每條邊兩個(gè))。
union-find 算法
CC 中基于深度優(yōu)先搜索來解決圖連通性問題的方法與 union-find算法 中的算法相比,理論上,深度優(yōu)先搜索更快,因?yàn)樗鼙WC所需的時(shí)間是常數(shù)而 union-find算法不行;但在實(shí)際應(yīng)用中,這點(diǎn)差異微不足道。union-find算法其實(shí)更快,因?yàn)樗恍枰暾貥?gòu)造表示一幅圖。更重要的是,union-find算法是一種動(dòng)態(tài)算法(我們?cè)谌魏螘r(shí)候都能用接近常數(shù)的時(shí)間檢查兩個(gè)頂點(diǎn)是否連通,甚至是添加一條邊的時(shí)候),但深度優(yōu)先搜索則必須對(duì)圖進(jìn)行預(yù)處理。
因此,我們?cè)谥恍枰袛噙B通性或是需要完成大量連通性查詢和插入操作混合等類似的任務(wù)時(shí),更傾向使用union-find算法,而深度優(yōu)先搜索則適合實(shí)現(xiàn)圖的抽象數(shù)據(jù)類型,因?yàn)樗芨行У乩靡延械臄?shù)據(jù)結(jié)構(gòu)。
使用深度優(yōu)先搜索還可以解決 檢測(cè)環(huán) 和雙色問題:
檢測(cè)環(huán),給定的圖是無環(huán)圖嗎?
namespace Graphs { public class Cycle { private bool[] marked; private bool hasCycle; public Cycle(Graph G) { marked = new bool[G.V()]; for (var s = 0; s < G.V(); s++) { if (!marked[s]) Dfs(G,s,s); } } private void Dfs(Graph g, int v, int u) { marked[v] = true; foreach (var w in g.Adj(v)) { if (!marked[w]) Dfs(g, w, v); else if (w != u) hasCycle = true; } } public bool HasCycle() { return hasCycle; } } }
是二分圖嗎?(雙色問題)
namespace Graphs { public class TwoColor { private bool[] marked; private bool[] color; private bool isTwoColorable = true; public TwoColor(Graph G) { marked = new bool[G.V()]; color = new bool[G.V()]; for(var s = 0;s<G.V();s++) { if (!marked[s]) Dfs(G,s); } } private void Dfs(Graph g, int v) { marked[v] = true; foreach (var w in g.Adj(v)) { if (!marked[w]) { color[w] = !color[v]; Dfs(g, w); } else if (color[w] == color[v]) isTwoColorable = false; } } public bool IsBipartite() { return isTwoColorable; } } }
8.符號(hào)圖
在典型應(yīng)用中,圖都是通過文件或者網(wǎng)頁定義的,使用的是字符串而非整數(shù)來表示和指代頂點(diǎn)。為了適應(yīng)這樣的應(yīng)用,我們使用符號(hào)圖。符號(hào)圖的API:
這份API 定義一個(gè)構(gòu)造函數(shù)來讀取并構(gòu)造圖,用 name() 和 index() 方法將輸入流中的頂點(diǎn)名和圖算法使用的頂點(diǎn)索引對(duì)應(yīng)起來。
實(shí)現(xiàn)
需要用到3種數(shù)據(jù)結(jié)構(gòu):
- 1.一個(gè)符號(hào)表 st ,鍵的類型為 string(頂點(diǎn)名),值的類型 int (索引);
- 2.一個(gè)數(shù)組 keys[ ],用作反向索引,保存每個(gè)頂點(diǎn)索引對(duì)應(yīng)的頂點(diǎn)名;
- 3.一個(gè) Graph 對(duì)象 G,它使用索引來引用圖中頂點(diǎn)。
SymbolGraph 會(huì)遍歷兩遍數(shù)據(jù)來構(gòu)造以上數(shù)據(jù)結(jié)構(gòu),這主要是因?yàn)闃?gòu)造 Graph 對(duì)象需要頂點(diǎn)總數(shù) V。在典型的實(shí)際應(yīng)用中,在定義圖的文件中指明 V 和 E 可能會(huì)有些不便,而有了 SymbolGraph,就不需要擔(dān)心維護(hù)邊或頂點(diǎn)的總數(shù)。
namespace Graphs { public class SymbolGraph { private Dictionary<string, int> st;//符號(hào)名 -> 索引 private string[] keys;//索引 -> 符號(hào)名 private Graph G; public SymbolGraph(string fileName, string sp) { var strs = File.ReadAllLines(fileName); st = new Dictionary<string, int>(); //第一遍 foreach (var str in strs) { var _strs = str.Split(sp); foreach (var _str in _strs) { st.Add(_str,st.Count); } } keys = new string[st.Count]; foreach (var name in st.Keys) { keys[st[name]] = name; } //第二遍 將每一行的第一個(gè)頂點(diǎn)和該行的其他頂點(diǎn)相連 foreach (var str in strs) { var _strs = str.Split(sp); int v = st[_strs[0]]; for (var i = 1; i < _strs.Length; i++) { G.AddEdge(v,st[_strs[i]]); } } } public bool Contains(string s) { return st.ContainsKey(s); } public int Index(string s) { return st[s]; } public string Name(int v) { return keys[v]; } public Graph Gra() { return G; } } }
間隔的度數(shù)
可以使用SymbolGraph 和 BreadthFirstPaths 來查找圖中的最短路徑:
總結(jié)
到此這篇關(guān)于C#圖表算法之無向圖的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
NGUI實(shí)現(xiàn)滑動(dòng)翻頁效果實(shí)例代碼
本文通過一段實(shí)例代碼給大家介紹NGUI實(shí)現(xiàn)滑動(dòng)翻頁效果,代碼簡(jiǎn)單易懂,對(duì)ngui 滑動(dòng)翻頁相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-04-04C#實(shí)現(xiàn)簡(jiǎn)易灰度圖和酷炫HeatMap熱力圖winform(附DEMO)
本文主要介紹了C#實(shí)現(xiàn)簡(jiǎn)易灰度圖和酷炫HeatMap熱力圖winform(附DEMO),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12淺拷貝和深拷貝深入理解(shallow copy VS deep copy)
淺拷貝和深拷貝深入理解(shallow copy VS deep copy) 本文重點(diǎn)討論引用類型變量的拷貝機(jī)制和實(shí)現(xiàn)2014-01-01WPF實(shí)現(xiàn)Badge標(biāo)識(shí)的示例代碼
這篇文章主要為大家詳細(xì)介紹了WPF如何實(shí)現(xiàn)Badge標(biāo)識(shí),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)或工作有一定幫助,感興趣的小伙伴可以了解一下2023-06-06使用C#9中records作為強(qiáng)類型ID的實(shí)例教程
這篇文章主要給大家介紹了關(guān)于使用C#9中records作為強(qiáng)類型ID的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01