繁体   English   中英

无向图中的循环

[英]Cycles in an Undirected Graph

给定一个有n个顶点 (| V | = n ) 的无向图G =( V , E ),你如何确定它是否包含O ( n ) 中的环?

我认为深度优先搜索可以解决这个问题。 如果一条未探索的边导致之前访问过的节点,则该图包含一个循环。 此条件也使其成为 O(n),因为您可以探索最大 n 条边而无需将其设置为 true 或留下任何未探索的边。

实际上,深度优先(或实际上广度优先)搜索还不够。 你需要一个看起来更复杂的算法。

例如,假设有一个节点 {a,b,c,d} 和边 {(a,b),(b,c),(b,d),(d,c)} 的图,其中边 (x ,y) 是从 x 到 y 的边。 (看起来像这样,所有边缘都朝下。)

    (a)
     |
     |
    (b)
    / \ 
  (d)  |
   |   |
    \ /
    (c)

然后进行深度优先搜索可能访问节点(a),然后(b),然后(c),然后回溯到(b),然后访问(d),最后再次访问(c)并得出结论有一个循环 -当没有。 广度优先也会发生类似的事情。

您需要做的是跟踪您在访问过程中的哪些节点。 在上面的示例中,当算法到达 (d) 时,它已完成访问 (c) 但未完成 (a) 或 (b)。 所以重新访问一个完成的节点是好的,但访问一个未完成的节点意味着你有一个循环。 通常的做法是将每个节点涂成白色(尚未访问)、灰色(访问后代)或黑色(已完成访问)。

这是一些伪代码!

define visit(node n):
  if n.colour == grey: //if we're still visiting this node or its descendants
    throw exception("Cycle found")

  n.colour = grey //to indicate this node is being visited
  for node child in n.children():
    if child.colour == white: //if the child is unexplored
      visit(child)

  n.colour = black //to show we're done visiting this node
  return

然后运行 ​​visit(root_node) 当且仅当存在循环时才会抛出异常(最初所有节点都应该是白色的)。

没有环的连通无向图 G 是一棵树! 任何树都恰好有 n-1 条边,因此我们可以简单地遍历图的边列表并计算边数。 如果我们计算 n − 1 条边,则返回“是”,但如果我们到达第 n 条边,则返回“否”。 这需要 O (n) 时间,因为我们最多查看 n 条边。

但是如果图没有连通,那么我们就必须使用 DFS。 我们可以遍历边,如果任何未探索的边通向访问过的顶点,那么它就有循环。

您可以使用 DFS 解决它。 时间复杂度:O(n)

该算法的本质是,如果连接组件/图不包含 CYCLE,则它将始终是 TREE。 看这里证明

让我们假设图没有环,即它是一棵树。 如果我们看一棵树,来自一个节点的每条边:

1.either 到达其唯一的父级,它比它高一级。

2. 或到达它的子级,它比它低一级。

因此,如果一个节点有任何其他不属于上述两条边的边,它显然会将该节点连接到其父节点以外的一个祖先节点。 这将形成一个循环。

现在事实已经清楚了,您所要做的就是为图运行 DFS(考虑到您的图是连接的,否则对所有未访问的顶点执行此操作),并且如果您找到已访问的节点的邻居而不是它的父母,那么我的朋友,图中有一个 CYCLE,您就完成了。

当您对其邻居执行 DFS 时,您可以通过简单地将父项作为参数传递来跟踪父项。 并且由于您最多只需要检查n 条边,因此时间复杂度将为 O(n)。

希望答案有所帮助。

顺便说一下,如果你碰巧知道它是连通的,那么它就是一棵树(因此没有循环)当且仅当|E|=|V|-1 当然,这不是少量的信息:)

答案是,真的,广度优先搜索(或深度优先搜索,这并不重要)。 细节在于分析。

现在,算法有多快?

首先,假设图形没有环。 边的数量是 O(V),图是一个森林,目标达到。

现在,假设图形有循环,您的搜索算法将在第一个循环中完成并报告成功。 该图是无向图,因此,当算法检查一条边时,只有两种可能性:要么它已经访问了边的另一端,要么它已经访问了这条边,然后这条边闭合了一个圆。 一旦它看到边缘的另一个顶点,该顶点就会被“检查”,因此这些操作只有 O(V) 次。 在整个算法运行过程中,第二种情况只会出现一次。

带条件的 DFS 方法(父级!= 下一个节点)让我们看看代码,然后了解发生了什么:

bool Graph::isCyclicUtil(int v, bool visited[], int parent) 
{ 
    // Mark the current node as visited 
    visited[v] = true; 

    // Recur for all the vertices adjacent to this vertex 
    list<int>::iterator i; 
    for (i = adj[v].begin(); i != adj[v].end(); ++i) 
    { 
        // If an adjacent is not visited, then recur for that adjacent 
        if (!visited[*i]) 
        { 
           if (isCyclicUtil(*i, visited, v)) 
              return true; 
        } 

        // If an adjacent is visited and not parent of current vertex, 
        // then there is a cycle. 
        else if (*i != parent) 
           return true; 
    } 
    return false; 
} 

上面的代码解释了自己,但我会尝试解释一个条件,即 *i != parent 如果假设图是

1--2

然后,当我们在 1 并转到 2 时,2 的父节点变为 1,当我们返回 1 时,因为 1 在 2 的 adj 矩阵中,那么由于下一个顶点 1 也是 2 的父节点,因此不会检测到循环此 DFS 方法中的直接父级。 因此代码工作正常

我相信图是连通的假设可能是少数。 因此,您可以使用上面显示的证明,即运行时间为 O(|V|)。 如果不是,则 |E|>|V|。 提醒:DFS 的运行时间是O(|V|+|E|)

这是我基于 DFS 用 C 编写的代码,用于确定给定的图是否连接/循环。 最后有一些示例输出。 希望它会有所帮助:)

#include<stdio.h>
#include<stdlib.h>

/****Global Variables****/
int A[20][20],visited[20],v=0,count=0,n;
int seq[20],s=0,connected=1,acyclic=1;

/****DFS Function Declaration****/
void DFS();

/****DFSearch Function Declaration****/
void DFSearch(int cur);

/****Main Function****/
int main() 
   {    
    int i,j;

    printf("\nEnter no of Vertices: ");
    scanf("%d",&n);

    printf("\nEnter the Adjacency Matrix(1/0):\n");
    for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
        scanf("%d",&A[i][j]);

    printf("\nThe Depth First Search Traversal:\n");

    DFS();

    for(i=1;i<=n;i++)
        printf("%c,%d\t",'a'+seq[i]-1,i);

    if(connected && acyclic)    printf("\n\nIt is a Connected, Acyclic Graph!");
    if(!connected && acyclic)   printf("\n\nIt is a Not-Connected, Acyclic Graph!");
    if(connected && !acyclic)   printf("\n\nGraph is a Connected, Cyclic Graph!");
    if(!connected && !acyclic)  printf("\n\nIt is a Not-Connected, Cyclic Graph!");

    printf("\n\n");
    return 0;
   }

/****DFS Function Definition****/
void DFS()
    { 
    int i;
    for(i=1;i<=n;i++)
        if(!visited[i])
          {
        if(i>1) connected=0;
        DFSearch(i);    
              } 
    }

/****DFSearch Function Definition****/
void DFSearch(int cur) 
    {
    int i,j;
    visited[cur]=++count;

        seq[count]=cur; 
        for(i=1;i<count-1;i++)
                if(A[cur][seq[i]]) 
                   acyclic=0;

    for(i=1;i<=n;i++)
        if(A[cur][i] && !visited[i])
           DFSearch(i);

    }

/*样本输出:

majid@majid-K53SC:~/Desktop$ gcc BFS.c

majid@majid-K53SC:~/Desktop$ ./a.out
************************************

Enter no of Vertices: 10

Enter the Adjacency Matrix(1/0):

0 0 1 1 1 0 0 0 0 0 
0 0 0 0 1 0 0 0 0 0 
0 0 0 1 0 1 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 1 0 0 1 0 0 0 0 0 
0 0 0 0 0 0 0 1 0 0 
0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 1 
0 0 0 0 0 0 1 0 0 0 

The Depdth First Search Traversal:
a,1 c,2 d,3 f,4 b,5 e,6 g,7 h,8 i,9 j,10    

It is a Not-Connected, Cyclic Graph!


majid@majid-K53SC:~/Desktop$ ./a.out
************************************

Enter no of Vertices: 4

Enter the Adjacency Matrix(1/0):
0 0 1 1
0 0 1 0
1 1 0 0
0 0 0 1

The Depth First Search Traversal:
a,1 c,2 b,3 d,4 

It is a Connected, Acyclic Graph!


majid@majid-K53SC:~/Desktop$ ./a.out
************************************

Enter no of Vertices: 5

Enter the Adjacency Matrix(1/0):
0 0 0 1 0
0 0 0 1 0
0 0 0 0 1
1 1 0 0 0 
0 0 1 0 0

The Depth First Search Traversal:
a,1 d,2 b,3 c,4 e,5 

It is a Not-Connected, Acyclic Graph!

*/

一个简单的 DFS 会检查给定的无向图是否有环。

这是相同的 C++ 代码。

上面代码中使用想法是:

如果一个已经被发现/访问过的节点再次被发现并且不是父节点,那么我们就有了一个循环。

这也可以解释如下(@Rafał Dowgird 提到

如果一条未探索的边导致之前访问过的节点,则该图包含一个循环。

如果 DFS 不产生后边,则无向图是无环的(即森林)。 由于后边是在深度优先树中将顶点u连接到祖先v那些边 ( u , v ),所以没有后边意味着只有树边,所以没有循环。 所以我们可以简单地运行 DFS。 如果找到后缘,则存在循环。 复杂度是O(V)而不是O(E + V) 因为如果有后边,必须在看到|V|之前找到它|V| 明显的边缘。 这是因为在无环(无向)森林中, |E| ≤ |V| + 1 |E| ≤ |V| + 1 |E| ≤ |V| + 1

正如其他人所提到的......深度优先搜索将解决它。 一般来说,深度优先搜索需要 O(V + E),但在这种情况下,您知道该图最多有 O(V) 条边。 所以你可以简单地运行一个 DFS,一旦你看到一个新的边缘,就会增加一个计数器。 当计数器达到 V 时,您不必继续,因为该图肯定有一个循环。 显然这需要 O(v)。

我相信正确使用 DFS 还取决于您将如何在代码中表示您的图形。 例如,假设您使用相邻列表来跟踪邻居节点,并且您的图形有 2 个顶点和只有一条边:V={1,2} 和 E={(1,2)}。 在这种情况下,从顶点 1 开始,DFS 会将其标记为 VISITED 并将 2 放入队列中。 之后它将弹出顶点 2,并且由于 1 与 2 相邻,并且 1 被访问,DFS 将得出结论,存在一个循环(这是错误的)。 换句话说,在无向图中 (1,2) 和 (2,1) 是相同的边,您应该以一种方式进行编码,让 DFS 不要将它们视为不同的边。 为每个访问过的节点保留父节点将有助于处理这种情况。

我最近开始学习图表。 我用java写了一段代码,可以确定一个图是否有循环。 我使用 DFT 在图中找到循环。 我没有使用递归,而是使用堆栈来遍历图形。

使用堆栈的高级 DFT 在以下步骤中完成

  1. 访问节点
  2. 如果该节点不在访问列表中,则将其添加到列表中并将其压入栈顶
  3. 将栈顶的节点标记为当前节点。
  4. 对当前节点的每个相邻节点重复以上操作
  5. 如果所有节点都被访问过,则将当前节点从堆栈中弹出

我对图的每个节点执行了 DFT,在遍历过程中,如果遇到我之前访问过的顶点,我会检查该顶点的堆栈深度是否大于 1。 我还检查了一个节点是否有自己的边,以及节点之间是否有多个边。 我最初写的堆栈版本不是很优雅。 我阅读了如何使用递归完成的伪代码,它很简洁。 这是一个java实现。 LinkedList 数组表示一个图形。 每个节点及其相邻顶点分别由数组和每个项目的索引表示

class GFG {
Boolean isCyclic(int V, LinkedList<Integer>[] alist) {
    List<Integer> visited = new ArrayList<Integer>();
    for (int i = 0; i < V; i++) {
        if (!visited.contains(i)) {
            if (isCyclic(i, alist, visited, -1))
                return true;
        }
    }
    return false;
}

Boolean isCyclic(int vertex, LinkedList<Integer>[] alist, List<Integer> visited, int parent) {
    visited.add(vertex);
    for (Iterator<Integer> iterator = alist[vertex].iterator(); iterator.hasNext();) {
        int element = iterator.next();
        if (!visited.contains(element)) {
            if (isCyclic(element, alist, visited, vertex))
                return true;
        } else if (element != parent)
            return true;
    }
    return false;
}

}

这是算法的 C++ 中的一个简单实现,用于检查图是否在O(n)时间内具有循环(n 是图中的顶点数)。 我没有在这里展示 Graph 数据结构的实现(为了保持简短的回答)。 算法期望 Graph 类具有公共方法, vector<int> getAdj(int v)返回与v相邻的顶点, int getV()返回顶点总数。 此外,这些算法假设图的顶点编号为0 to n - 1

class CheckCycleUndirectedGraph
{
private:
    bool cyclic;
    vector<bool> visited;

    void depthFirstSearch(const Graph& g, int v, int u) {
        visited[v] = true;
        for (auto w : g.getAdj(v)) {
            if (!visited[w]) {
                depthFirstSearch(g, w, v);
            }
            else if (w != u) {
                cyclic = true;
                return;
            }
        }
    }

public:
    CheckCycleUndirectedGraph(const Graph& g) : cyclic(false) {
        visited = vector<bool>(g.getV(), false);
        for (int v = 0; v < g.getV(); v++) {
            if (!visited[v]){
                depthFirstSearch(g, v, v);
                if(cyclic)
                  break;
            }
        }
    }

    bool containsCycle() const {
        return cyclic;
    }
};

请记住,Graph 可能由多个未连接的组件组成,并且组件内部可能存在循环。 所示算法也检测此类图中的循环。

您可以使用boost 图库循环依赖 它具有使用back_edge函数查找循环的解决方案。

无环无向图有|E| < |V|-1。

public boolean hasCycle(Graph g) {

   int totalEdges = 0;
   for(Vertex v : g.getVertices()) {
     totalEdges += v.getNeighbors().size();
   }
   return totalEdges/2 > g.getVertices().size - 1;

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM