重拾算法(4)——圖的廣度優先和深度優先搜索算法的實現與33867個測試用例
本篇繼續上一篇的方式,給出圖的深度優先和廣度優先搜索算法,然后用33867個測試用例進行自動化測試,以證明算法的正確性。
用鄰接表(adjacency list)表示圖(graph)
1 public partial class AdjacencyListGraph<TVertex, TEdge> : ICloneable 2 { 3 public AdjacencyListGraph() 4 { 5 this.Vertexes = new List<AdjacencyListVertex<TVertex, TEdge>>(); 6 } 7 8 public IList<AdjacencyListVertex<TVertex, TEdge>> Vertexes { get; protected set; } 9 10 /* 略 */ 11 } 12 13 public class AdjacencyListVertex<TVertex, TEdge> 14 { 15 public TVertex Value { get;set; } 16 public IList<AdjacencyListEdge<TVertex, TEdge>> Edges { get;set; } 17 18 public AdjacencyListVertex() 19 { 20 this.Edges = new List<AdjacencyListEdge<TVertex, TEdge>>(); 21 } 22 } 23 24 public class AdjacencyListEdge<TVertex, TEdge> 25 { 26 public TEdge Value { get;set; } 27 public AdjacencyListVertex<TVertex, TEdge> Vertex1 { get;set; } 28 public AdjacencyListVertex<TVertex, TEdge> Vertex2 { get;set; } 29 30 public AdjacencyListEdge(AdjacencyListVertex<TVertex, TEdge> vertex1, AdjacencyListVertex<TVertex, TEdge> vertex2) 31 { 32 this.Vertex1 = vertex1; 33 this.Vertex2 = vertex2; 34 } 35 }
圖的廣度優先算法
圖的廣度優先算法和樹的層次遍歷是類似的。
1 SearchReport<TVertex, TEdge> BreadthFirstTraverse(GraphNodeWorker<TVertex, TEdge> worker, bool reportNeeded) 2 { 3 SearchReport<TVertex, TEdge> result = null; 4 if (reportNeeded) { result = new SearchReport<TVertex, TEdge>(); } 5 var visited = new Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool>(); 6 foreach (var vertex in this.Vertexes) 7 { 8 if ((!visited.ContainsKey(vertex)) || (!visited[vertex])) 9 { 10 BFS(vertex, visited, worker); 11 if (reportNeeded) { result.ConnectedComponents.Add(vertex); } 12 } 13 } 14 return result; 15 } 16 17 void BFS(AdjacencyListVertex<TVertex, TEdge> headNode, Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool> visited, GraphNodeWorker<TVertex, TEdge> worker) 18 { 19 var queue = new Queue<AdjacencyListVertex<TVertex, TEdge>>(); 20 queue.Enqueue(headNode); 21 while (queue.Count > 0) 22 { 23 var vertex = queue.Dequeue(); 24 if ((!visited.ContainsKey(vertex)) || (!visited[vertex])) 25 { 26 if (vertex != null) 27 { 28 worker.DoActionOnNode(vertex); 29 if (!visited.ContainsKey(vertex)) 30 { visited.Add(vertex, true); } 31 else 32 { visited[vertex] = true; } 33 var neighbourVertexes = from edge in vertex.Edges 34 select GetNeighbourVertex(vertex, edge); 35 foreach (var v in neighbourVertexes) 36 { 37 if ((!visited.ContainsKey(v)) || (!visited[v])) 38 { queue.Enqueue(v); } 39 } 40 } 41 } 42 } 43 }
其中的SearchReport<TVertex, TEdge>是一個統計搜索結果的對象,定義如下
1 public class SearchReport<TVertex, TEdge> 2 { 3 public List<AdjacencyListVertex<TVertex, TEdge>> ConnectedComponents { get;set; } 4 public SearchReport() 5 { 6 ConnectedComponents = new List<AdjacencyListVertex<TVertex, TEdge>>(); 7 } 8 }
ConnectedComponents有多少個元素,就表示這個圖有多少個連通分量。
圖的深度優先搜索算法
圖的深度優先搜索可以用"遞歸"、"棧"和"優化的棧"三種形式實現。
1 SearchReport<TVertex, TEdge> DepthFirstTraverse(GraphNodeWorker<TVertex, TEdge> worker, bool reportNeeded, DepthFirstTraverseOption option) 2 { 3 SearchReport<TVertex, TEdge> result = null; 4 if (reportNeeded) { result = new SearchReport<TVertex, TEdge>(); } 5 var visited = new Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool>(); 6 foreach (var vertex in this.Vertexes) 7 { 8 if ((!visited.ContainsKey(vertex)) || (!visited[vertex])) 9 { 10 switch (option) 11 { 12 case DepthFirstTraverseOption.DFSRecursively: 13 DFS(vertex, visited, worker); 14 break; 15 case DepthFirstTraverseOption.DFSByStack: 16 DFSByStack(vertex, visited, worker); 17 break; 18 case DepthFirstTraverseOption.DFSByStackOptimized: 19 DFSByStackOptimized(vertex, visited, worker); 20 break; 21 default: 22 throw new NotImplementedException(); 23 } 24 if (reportNeeded) { result.ConnectedComponents.Add(vertex);} 25 } 26 } 27 return result; 28 }
用遞歸實現深度優先搜索
1 void DFS(AdjacencyListVertex<TVertex, TEdge> vertex, Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool> visited, GraphNodeWorker<TVertex, TEdge> worker) 2 { 3 //if ((!visited.ContainsKey(vertex)) || (!visited[vertex])) 4 { 5 worker.DoActionOnNode(vertex); 6 if (!visited.ContainsKey(vertex)) 7 { visited.Add(vertex, true); } 8 else 9 { visited[vertex] = true; } 10 var neighbourVertexes = from edge in vertex.Edges 11 select GetNeighbourVertex(vertex, edge); 12 foreach (var v in neighbourVertexes) 13 { 14 if ((!visited.ContainsKey(v)) || (!visited[v])) 15 { DFS(v, visited, worker); } 16 } 17 } 18 }
其中GetNeighbourVertex是個輔助函數,用於獲取與指定結點相連的結點。
1 AdjacencyListVertex<TVertex, TEdge> GetNeighbourVertex(AdjacencyListVertex<TVertex, TEdge> vertex, AdjacencyListEdge<TVertex, TEdge> edge) 2 { 3 if (vertex == null || edge == null) { return null; } 4 Debug.Assert(!((vertex != edge.Vertex1) && (vertex != edge.Vertex2))); 5 6 AdjacencyListVertex<TVertex, TEdge> result = null; 7 if (vertex != edge.Vertex1) { result = edge.Vertex1; } 8 else { result = edge.Vertex2; } 9 10 return result; 11 }
用棧實現深度優先搜索
1 void DFSByStack(AdjacencyListVertex<TVertex, TEdge> root, Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool> visited, GraphNodeWorker<TVertex, TEdge> worker) 2 { 3 var stack = new Stack<AdjacencyListVertex<TVertex, TEdge>>(); 4 stack.Push(root); 5 6 while (stack.Count > 0) 7 { 8 var vertex = stack.Pop(); 9 if (vertex != null) 10 { 11 if ((!visited.ContainsKey(vertex)) || (!visited[vertex])) 12 { 13 worker.DoActionOnNode(vertex); 14 if (!visited.ContainsKey(vertex)) 15 { visited.Add(vertex, true); } 16 else 17 { visited[vertex] = true; } 18 19 var neighbourVertexes = from edge in vertex.Edges 20 select GetNeighbourVertex(vertex, edge); 21 foreach (var v in neighbourVertexes.Reverse()) 22 { 23 if ((!visited.ContainsKey(v)) || (!visited[v])) 24 { 25 stack.Push(v); 26 } 27 } 28 } 29 } 30 } 31 }
這個用棧實現的深度優先搜索算法,其特點是與上文用遞歸實現的算法相比,兩者對圖上結點的遍歷順序完全相同。因此我用這個兩個算法對比以驗證他們兩個是否正確。
優化過的用棧實現深度優先搜索
這個用棧實現的深度優先搜索算法還有可優化的空間。優化后的算法如下。
1 void DFSByStackOptimized(AdjacencyListVertex<TVertex, TEdge> root, Dictionary<AdjacencyListVertex<TVertex, TEdge>, bool> visited, GraphNodeWorker<TVertex, TEdge> worker) 2 { 3 var stack = new Stack<AdjacencyListVertex<TVertex, TEdge>>(); 4 stack.Push(root); 5 if (!visited.ContainsKey(root)) { visited.Add(root, false); } 6 else { visited[root] = false; } 7 8 while (stack.Count > 0) 9 { 10 var vertex = stack.Pop(); 11 if (vertex != null) 12 { 13 worker.DoActionOnNode(vertex); 14 visited[vertex] = true; 15 var neighbourVertexes = from edge in vertex.Edges 16 select GetNeighbourVertex(vertex, edge); 17 foreach (var v in neighbourVertexes) 18 { 19 if (!visited.ContainsKey(v)) 20 { 21 stack.Push(v); 22 visited.Add(v, false); 23 } 24 } 25 } 26 } 27 }
這一版的算法,避免了不必要的入棧出棧,減少了對visited的判定次數,去掉了不必要的Reverse()。
要注意的是,優化后的算法,對圖上結點的遍歷順序與優化前有所不同。
測試
我的測試思路如下:
-
編程自動生成具有1、2、3、4、5、6個結點的圖的所有情形(一共有33867個。結點數目相同時,連線的不同意味着情形的不同)
-
打印33867個圖的情形。
-
對33867個圖,分別進行基於遞歸和棧的深度優先搜索,若搜索結果完全相同,就說明這兩個算法是正確的。
-
在上一步基礎上,若基於優化的棧的深度優先搜索結果與上一步的搜索結果相比,只有訪問順序不同,就說明基於優化的棧的算法是正確的。
-
在上一步基礎上,若廣度優先搜索結果與上一步的遍歷結果相比,只有訪問順序不同,就說明廣度優先搜索算法是正確的。
自動生成33867個不同的圖
這個程序的實現思路與上一篇是一樣的。在得到了所有具有N個結點的圖后,給每個圖增加一個結點,就成了N+1個結點的新圖,一個這樣的新圖可以擴展出2^N個新的情形。而最初的具有1個結點的圖就只有那么1個。利用數學歸納法,生成33867個不同的圖的問題就解決了。
在控制台顯示圖結構
在控制台顯示一個二叉樹結構還算常見,但要顯示圖就復雜一點。我設計了按如下形式顯示圖結構。
1 graph 9485: 2 component 0: 3 000 4 ┕┑ 5 001│ 6 ┕┙ 7 8 component 1: 9 002 10 ┝┑ 11 ┕┿┑ 12 003││ 13 ┝┙│ 14 ┕━┿┑ 15 004 ││ 16 ┝━┙│ 17 ┕━━┙ 18 19 component 2: 20 005
受字體影響可能看不出效果,把上述內容復制到notepad里是這樣的:
可見這個圖是生成的第9485個圖。圖中的"001""002""003""004"是結點,黑線代表邊。它有3個連通分量(component)。其中component0包含2個結點和1條邊,component1包含3個結點和3條邊,component2包含1個結點,不含邊。
這樣直觀地看到圖的結構,就容易進行排錯調試了。
至於遍歷、比較、判定是否正確的程序,就沒有什么新意可言了。
總結
沒有這樣的測試,我是不敢相信我的算法實際可用的。雖然為了測試花掉好幾天時間,不過還是很值得的。現在我可以放心大膽地說,我給出的圖的廣度優先和深度優先搜索算法是真正正確的!
需要工程源碼的同學麻煩點個贊並留言你的Email~