如何在C#中检测图形中的周期

134 阅读9分钟

用C#语言进行图形中的周期检测

图是最通用的数据结构之一。这是因为它们允许我们解决有趣的问题。它们被用于社交网络和GPS应用中。

人们可以在任何你想对一堆对象之间的关系进行建模的地方应用它。在这篇文章中,我们主要关注的是有循环的图。了解这个概念对帮助我们检测计算机程序中的无限循环很重要。

前提条件

为了能够很好地学习这篇文章,人们需要。

  • 安装有[Visual Studio]。
  • 对[递归]有一定的了解。
  • 对如何使用[邻接列表]和矩阵构建图有一定了解。
  • 对C#或任何面向对象的编程语言有基本了解。

主要收获

  • 简要介绍一下图。
  • 理解图中的循环是什么。
  • 理解如何检测一个周期。
  • 深度优先搜索算法。
  • 有向图上的周期检测。
  • 无向图上的周期检测。
  • 理解周期检测的不同应用。

图的简要概述

图就像一棵树,但没有任何周期。我们在图中没有一个根节点。下面是一个有四个节点或顶点和六条边或线的图的例子。我们没有限制从一个节点可以有多少个连接。

如果两个节点是相连的,我们就说它们是相邻的或邻居。Johnbob 是邻居。JohnSam 不是,因为它们没有连接。

image of a Graph

如果边有一个方向,我们就说我们有一个有向图。从下面这个有向图的图片来看,JohnBob 是相连的,但反过来就不是了。这就是Twitter的工作方式。如果你关注某人,就有一个从你的账户到他们账户的连接。而不是相反,除非他们也关注你。

image of a directed-graph

也有不定向的图。这方面的一个例子可以是Facebook。当你添加一个朋友时,有一个从你到他们的连接。这句话是对的。这些连接没有方向。

边缘也可以有权重。我们用这些权重来代表连接的强度,例如,在Facebook上,当两个人交流时,我们可以给他们加更多的权重。然后我们利用这一点,用权重最高的节点来显示他们最好的朋友。

Image of Weighted Graph

通过这个简单的描述,我们可以更好地理解什么是循环,以及如何在图中检测到它。

什么是图中的循环?

图中的循环是指第一个和最后一个顶点是相同的。如果从一个顶点出发,沿着一条路径行进,最后到达起始顶点,那么这条路径就是一个循环。

循环检测是寻找一个循环的过程。

在我们下面的例子中,我们的路径134 ,再回到1 ,这就是一个循环。在图的上部没有循环。这些是路径126127 。在任何搜索中,如果你知道有可能出现循环,你需要管理它。

当这一点没有得到管理时,你的算法将无限次地运行。这将导致StackOverflow 异常错误。

Image of Cycle Graph

一个关于无限循环如何发生的例子

让我们查一下节点6 。如果我们开始搜索,通过134 的路径,我们将回到1 。在这一点上,我们已经检测到一个循环。没有办法结束它。这个循环将继续下去。因此,我们将永远无法到达6

如何检测一个循环

要检测一个图中的循环,深度优先搜索算法是最好的算法。

深度优先搜索

深度优先搜索(DFS),是一种图的遍历方法。我们从一个特定的顶点开始搜索。然后我们探索所有其他的顶点,只要我们能沿着这个路径走。当到达该路径的终点时,我们做一个回溯,直到我们开始的那一点。

在进行回溯时,堆栈数据结构是最好的。

Image of DFS

DFS如何工作的例子

从上面的DFS图中,假设1 是我们的起始节点。我们看一下将出现在我们的邻接矩阵中的第一个项目。那就是2 。我们不排队在1 旁边的节点,而是排队在2 旁边的节点。所以我们将去6

如果6 是我们要找的节点,我们就停止。让我们假设它不是。我们回溯到2 。从2 ,我们寻找与它相连的其他节点。这就是7 。由于7 没有任何其他连接的节点,而且它也不是我们要找的东西。然后我们回溯到2 。我们已经完成了对所有与2 连接的节点的访问。因此我们回溯到1

1 ,我们检查还有哪个节点与它相连。我们有3 。我们访问节点3 并检查与它相连的节点。我们有8 。我们访问8 。它是最后一个节点。没有其他节点与它相连。如果它不是我们要找的,我们回溯到33 没有其他节点与它相连。我们回溯到1

我们再次从我们的列表中检查是否有任何其他节点连接到1 。我们有4 。我们访问44 没有任何其他节点与之相连。我们回溯到11 仍然有5 与它相连。我们访问5 ,并回溯到1 。我们已经完成了对所有节点的访问。

使用DFS实现周期检测

1.有向图中的循环检测

为了检测一个图中的循环,我们访问该节点,将其标记为已访问。然后访问通过它连接的所有节点。当访问一个被标记为已访问并属于当前路径的节点时,将检测到一个周期。

Image of Cycle Graph

以下是对如何检测周期的解释,以上面的周期图为参考。

我们声明两个布尔数组变量。一个被称为visited。另一个被称为路径。两个数组的大小都是顶点的数量。在这种情况下,7。路径变量是关键。这是因为它将帮助我们确定我们是否找到了一个循环。

这两个布尔数组变量首先被初始化为false。当我们访问每个节点时,我们将访问的变量标记为真。我们也将路径变量标记为真。

  1. 访问节点1 。把它标记为已访问。把它标记为我们当前路径的一部分。调用节点2
  2. 访问节点2 。把它标记为已访问。把它标记为我们当前路径的一部分。调用节点5
  3. 访问节点5 。把它标记为已访问。把它标记为我们当前路径的一部分。没有节点与它相连。把它从我们的当前路径中删除,在这个点/索引的路径变量上标记为false。将visited标记为true。返回到节点2
  4. 访问节点6 。把它标记为已访问。把它标记为我们当前路径的一部分。节点6 ,没有子节点。从我们当前的路径中移除它。保留它为已访问的真。
  5. 从节点2 返回到节点1 。从我们的当前路径中删除节点2 。这是因为我们已经完成了对所有与它相连的节点的访问。

我们已经访问了节点1,2,5,6 ,只有节点1 是当前路径的一部分。

请注意,所有这些节点都被标记为已访问。

  1. 从节点1 ,转到节点3 ,把它标记为已访问的节点,并且是我们当前路径的一部分。调用其子节点4
  2. 访问节点4 ,将其标记为已访问并属于我们当前路径的一部分。调用其子节点1

在这一点上,我们发现1 是我们当前路径的一部分。它也被标记为被访问。一个循环被检测出来了。

用C#实现循环检测的代码

  1. 打开Visual Studio。创建一个名为GraphCycleDetect 的控制台应用程序。
  2. Main() 方法中,我们创建一个锯齿状数组graph 。这将是容纳我们的顶点并显示它们是如何连接的。这就是我们将用来构建我们的邻接矩阵。它将是图形的一个代表。
  3. 我们调用MakeGraph() 函数。将顶点的数量和graph 作为参数传给该函数。下面是实现步骤1到3的代码。
 public static void Main(string[] args)
       {
 // Created the jagged array. It contains the vertexes and how they are to be connected.
 // E.g. new int[]{ 1,2}, means 1 is to be connected to 2
 int[][] graph = new int[][]
           {

 new int[]{ 1,2},

 new int[]{ 1,3},

 new int[]{ 1,4},

 new int[]{ 1,5},

 new int[]{ 2,6},

 new int[]{ 2,7},

 new int[]{ 3,4},

 new int[]{ 4,1},

           };
 // this is the total number of nodes in our graph
 int nodes = 8;
 // we call the MakeGraph method. This is the method that will constract the graph for us.
 var hasCycle = MakeGraph(graph, nodes);
 // This will print for us the result on the output window. If we find a cycle it will print:
 //Does the graph have a cycle ? ---> true
 // If the graph does not have a cycle it will print
 //Does the graph have a cycle ? ---> false
 Console.WriteLine($"Does the graph have a cycle ? ---> {hasCycle}");

 // A built-in c# Static method that reads the next line of character.
 //The result will stay on the output window when we add this line.
 Console.ReadLine();

       }
  1. MakeGraph() 函数中构建邻接矩阵。
  • 声明一个字典数据结构,ls 。这将存储我们的邻接列表/图。
  • 声明一个被访问的布尔变量,visited 。它的大小是节点的数量。
  • 声明一个路径布尔变量,path 。它的大小是节点的数量。
  • 用节点的值来填充字典。
  • 遍历节点。对于每个迭代。
  • 调用DFS() 函数。
  • 如果MakeGraph() 函数返回一个true ,那么就检测到了一个循环。否则,当它返回false ,就没有循环。

下面是上面第4步的代码实现。


 private static bool MakeGraph(int[][] graph, int nodes)
       {
 // This is the dictionary for storing the adjacency list.
 //It is of the type, int that will hold a node and List<int> that will hold all other nodes 
 //attached to the int node.
 /* E.G. This is how our graph will look like 
                    1-> 2,3,4
                    2-> 6,7
                    3-> 4
                    4-> 1
                */
 Dictionary<int, List<int>> ls = new Dictionary<int, List<int>>();
 // We declare a visited bool array variable. We will store the visited nodes in it.
 bool[] visited = new bool[nodes];
 // We declare a path bool array variable. We will store all nodes in our current path here.
 bool[] path = new bool[nodes];
 // Loop through our jagged array, graph.
 for (int i = 0; i < graph.Length; i++)
           {
 // As we loop, check whether our dictionary already contains the node at index[i][0]
 // of our jagged array, graph. If it is not there, we add it to the dictionary, ls
 if (!ls.ContainsKey(graph[i][0]))
               {
 ls.Add(graph[i][0], new List<int>());
               }
 // this line of code will connect the nodes. E.g. If we are given { 1,2}, we added 1 to our dictionary
 // on the line  ls.Add(graph[i][0], new List<int>());
 //Therefore, in this next line,ls[graph[i][0]].Add(graph[i][1]); we connect the 1 to the 2
 ls[graph[i][0]].Add(graph[i][1]);

           }
 // We start our traversal here. We could also say that this is where we start our path from.
 for (int i = 0; i < nodes; i++)
           {
 // We do our Dfs starting from the node at i in this case our start point will be 0;
 // For each Dfs, we are checking if we will find a cycle. If yes, we immediately return true.
 // A cycle has been found.
 if (Dfs(ls, i, visited, path))

 return true;

           }
 // If in our for loop above, we never found a cycle, then we will return false.
 // A cycle was not detected.
 return false;

       }
  1. 创建一个布尔函数DFS() ,检测是否有一个周期。下面是同样的代码。我们使用递归进行回溯。

如果检测到一个循环,我们返回true ,否则,我们返回false

 private static bool Dfs(Dictionary<int, List<int>> graph, int start, bool[] visited, bool[] path)
       {
 // If we find that we marked path[start] true, we return true.
 // This means that we have come back to the node we started from hence a cycle has
 // been found.
 if (path[start])
           {
 return true;
           }
 // If we didn't find a cycle from the code block above, we mark visited[start] to true.
 visited[start] = true;

 //  We also mark path[start] to true. This will help us know that the node start is on our
 //  current path.
 path[start] = true;

 // We check whether our graph contains the start node. Sometimes the start node is not in our graph.
 // Thus, if we do our traversal on such a node, an exception will be thrown. This is because the node does
 // not exist.
 if (graph.ContainsKey(start))
           {
 // We start our traversal from our start node of the graph.
 for each (var item in graph[start])

               {
 //We do our recursion
 // At this point, if the start node returned a true in our recursive call, then we say that cycle has been
 // found. We return true immediately.
 if (DFS(graph, item, visited, path))
                   {
 return true;
                   }

               }

           }
 // If we have traversed the whole path from the start node and never found a cycle, we start removing
 // those nodes from this path. This is done recursively using c# inbuilt stack also called the call stack.
 path[start] = false;
 // If we did not find a cycle, we return false.
 return false;

       }

上述代码运行后的输出将是。

Image of Output

2.无向图中的循环检测。

我们刚才谈到了在有向图中寻找一个周期。在无向图中,方法会有一些不同。

为什么?

下面我们有一个无向图的例子图片。这个图没有方向。这意味着,有一个从AB 的连接。还有一个从BA 的连接。使用以前的方法,我们从节点A 开始遍历。

我们访问A ,并将其标记为我们当前路径的一部分。然后我们访问与它相连的节点。这就是B 。我们把它标记为已访问并在当前路径集中。因为有一条从BA 的边,而且A 是当前路径的一部分。一个循环被检测出来了。

通过上述方法,在每两个由边连接的节点之间总是会检测到一个循环。我们需要为无向图想出一种dfs算法。总是在两条边之间找到一个周期。

Image of Undirected Graph

正确的DFS算法来检测无定向图中的周期

为了避免在两条边之间找到一个循环,我们要做以下工作。当我们遍历图形时,我们应该将每个节点的父节点传递给其邻居。这将防止返回到父节点。下面是一个详细的解释。

在下图中,我们添加了另一条边C ,形成一个循环。我们从任何一个节点开始我们的深度优先搜索。让我们说A 。我们把A 放在当前路径节点的集合中。然后我们探索A 的邻居。我们可以去BC 。顺序并不重要。

我们去BB 有两个邻居,AC 。我们不想再回到我们来的地方。当我们从A 移动到B ,我们应该通过A 作为父节点。当探索B 的邻居时,我们会知道我们来自A 。我们不会向A 的方向走。我们会去C

C 添加到当前路径集。我们把B 作为父节点。C 还有一个邻居A 。我们已经访问了A 。它也是我们当前路径集的一部分。一个周期被检测出来了。

Image of Undirected Graph

单向图中循环检测的代码实现

  1. 打开Visual Studio。创建一个名为UnidirectedGraphCycleDetect 的控制台应用程序。
  2. Main() 方法中,我们创建一个锯齿状数组graph 。这将保存我们的顶点并显示它们是如何连接的。
  3. 我们调用MakeGraph() 函数。将顶点的数量和graph 作为参数传给该函数。下面是实现步骤1到3的代码。

 public static void Main(string[] args)
       {

 char[][] graph = new char[][]
           {
 new char[]{ 'A','B'},

 new char[]{ 'B','A'},

 new char[]{ 'B','C'},

 new char[]{ 'C','B'},

 new char[]{ 'C','A'},

 new char[]{ 'A','C'},

           };

 int nodes = 4;

 var hasCycle = MakeGraph(graph, nodes);

 Console.WriteLine($"Does the graph have a cycle ? ---> {hasCycle}");

 Console.ReadLine();

       }
  1. MakeGraph() 函数中构建相邻矩阵。这个概念仍然与下面的函数MakeGraph() 的有向图的概念相同。区别在于变量的不同。在有向图的代码例子中,我们使用int 的值。这里我们使用的是char 的值。
 private static bool MakeGraph(char[][] graph, int nodes)
    {
 Dictionary<int, List<char>> ls = new Dictionary<int, List<char>>();

 bool[] visited = new bool[100];

 bool[] path = new bool[100];

 for (int i = 0; i < graph.Length; i++)
           {
 if (!ls.ContainsKey(graph[i][0]))
               {
 ls.Add(graph[i][0], new List<char>());
               }

 ls[graph[i][0]].Add(graph[i][1]);

           }

 for (int i = 0; i < ls.Count; i++)
           {
 if (HasCycle(ls, ls.ElementAt(i).Key, visited, -1))
 return true;
           }

 return false;

    }
  1. 我们将在我们现有的类中添加一个新的方法,叫做HasCycle()HasCyle() ,在这个例子中是Dfs函数。下面是在无向图中实现周期检测的代码。

请注意,在if (current != parent) ,我们首先检查我们是否要回到父节点。如果没有,no cycle 已经找到了。否则,a cycle is found

 public static bool HasCycle(Dictionary<int, List<char>> connections, int start, bool[] visited, int parent)
       {
 visited[start] = true;
 // Traverse the graph starting from the start node.
 foreach (var item in connections[start])
           {

 if (!visited[item])
               {
 // This is our recursive call. If in our recursion we find a cycle, we immediately return a true.
 if (HasCycle(connections, item, visited, start))

                   {
 return true;
                   }

               }

 var current = (int)item;
 // This is where the difference comes from the directed graph. As we traverse, we check whether the node is a parent.
 // If not then it means that a cycle has been found. We return true.
 if (current != parent)
               {
 return true;
               }

           }
 //If we haven't found a cycle we return false.
 return false;

       }

上述代码运行后的输出结果将是。

Image of Output

循环检测的应用

  • 它经常被用于基于分布式消息的算法中。
  • 在并发系统中用于检测死锁。
  • 在加密系统中使用。它确定了一个消息的密钥,可以将该相同的消息映射到相同的加密值。

总结

在这篇文章中,我们已经了解了DFS遍历。它是用于检测图中的循环的最佳算法。这是一个重要的概念,特别是当人们发现自己不得不应用它时。我希望这篇文章能使图中周期检测的概念变得清晰。