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