重拾算法(4)——圖的廣度優先和深度優先搜索算法的實現與33867個測試用例


重拾算法(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~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM