菜单
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE
     ❯   

DSA 图的循环检测


图中的循环

图中的循环是指一条路径,它从同一个顶点开始并结束于同一个顶点,其中不重复任何边。这就像在一个迷宫中行走,并最终回到了你开始的地方。

F B C A E D G

是否包含循环

循环的定义可能因情况而异。例如,一个自环,即一条边从同一个顶点出发并回到同一个顶点,可能被视为循环,也可能不被视为循环,这取决于您要解决的问题。


循环检测

能够检测图中的循环非常重要,因为在网络、调度和电路设计等许多应用中,循环可能表明存在问题或特殊情况。

最常见的两种检测循环的方法是:

  1. 深度优先搜索 (DFS):DFS 遍历会探索图并标记已访问的顶点。当当前顶点有一个已经访问过的邻接顶点时,就会检测到循环。
  2. 并查集 (Union-Find):该方法最初将每个顶点定义为一个组或一个子集。然后,对于每条边,将这些组连接起来。当探索一条新边时,如果两个顶点已经属于同一个组,则会检测到循环。

下面将更详细地解释 DFS 和并查集如何工作以及如何实现它们。


无向图的 DFS 循环检测

要使用深度优先搜索 (DFS) 检测无向图中的循环,我们使用的代码与上一页的 DFS 遍历代码 非常相似,只需做一些修改。

工作原理

  1. 对每个未访问的顶点开始 DFS 遍历(以防图不连通)。
  2. 在 DFS 过程中,将顶点标记为已访问,并递归地对邻接顶点运行 DFS。
  3. 如果一个邻接顶点已经被访问过,并且不是当前顶点的父节点,则检测到循环,并返回 True
  4. 如果对所有顶点都完成了 DFS 遍历并且没有检测到循环,则返回 False

运行下面的动画,看看 DFS 循环检测如何在一个特定的图上运行,从顶点 A 开始(这与之前的动画相同)。

F B C A E D G

是否包含循环

DFS 遍历从顶点 A 开始,因为它是邻接矩阵中的第一个顶点。然后,对于每个新访问的顶点,对所有尚未访问的邻接顶点递归调用遍历方法。当访问顶点 F 时,检测到循环,并发现邻接顶点 C 已经被访问过。

示例

Python

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size  

    def add_edge(self, u, v):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.adj_matrix[u][v] = 1
            self.adj_matrix[v][u] = 1

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

    def print_graph(self):
        print("Adjacency Matrix:")
        for row in self.adj_matrix:
            print(' '.join(map(str, row)))
        print("\nVertex Data:")
        for vertex, data in enumerate(self.vertex_data):
            print(f"Vertex {vertex}: {data}")
            
    def dfs_util(self, v, visited, parent):
        visited[v] = True

        for i in range(self.size):
            if self.adj_matrix[v][i] == 1:
                if not visited[i]:
                    if self.dfs_util(i, visited, v):
                        return True
                elif parent != i:
                    return True
        return False

    def is_cyclic(self):
        visited = [False] * self.size
        for i in range(self.size):
            if not visited[i]:
                if self.dfs_util(i, visited, -1):
                    return True
        return False

g = Graph(7)

g.add_vertex_data(0, 'A')
g.add_vertex_data(1, 'B')
g.add_vertex_data(2, 'C')
g.add_vertex_data(3, 'D')
g.add_vertex_data(4, 'E')
g.add_vertex_data(5, 'F')
g.add_vertex_data(6, 'G')

g.add_edge(3, 0)  # D - A
g.add_edge(0, 2)  # A - C
g.add_edge(0, 3)  # A - D
g.add_edge(0, 4)  # A - E
g.add_edge(4, 2)  # E - C
g.add_edge(2, 5)  # C - F
g.add_edge(2, 1)  # C - B
g.add_edge(2, 6)  # C - G
g.add_edge(1, 5)  # B - F

g.print_graph()

print("\nGraph has cycle:", g.is_cyclic())
运行示例 »

第 66 行:当调用 is_cyclic() 方法时,DFS 循环检测开始。

第 37 行:由于此时还没有访问任何顶点,因此 visited 数组首先被设置为所有顶点的 false

第 38-42 行:对图中的所有顶点运行 DFS 循环检测。这是为了确保所有顶点都被访问到,以防图不连通。如果一个节点已经被访问过,则必然存在循环,并返回 True。如果所有节点只被访问一次,这意味着没有检测到循环,则返回 False

第 24-34 行:这是 DFS 循环检测的一部分,它访问一个顶点,然后递归访问邻接顶点。如果一个邻接顶点已经被访问过,并且不是父节点,则检测到循环,并返回 True


有向图的 DFS 循环检测

要检测有向图中的循环,算法仍然与无向图非常相似,但代码必须稍作修改,因为对于有向图,如果我们遇到一个已经被访问过的邻接节点,并不一定意味着存在循环。

考虑下面的图,其中探索了两个路径,试图检测循环:

1 2 C B D A

在第一条路径(要探索的第一条路径)中,访问了顶点 A->B->C,没有检测到循环。

在要探索的第二条路径(路径 2)中,访问了顶点 D->B->C,并且该路径没有循环,对吗?但是,如果我们不对程序进行更改,当从 D 到邻接顶点 B 时,实际上会检测到错误的循环,因为 B 已经在路径 1 中被访问过了。为了避免这种错误的检测,代码被修改为仅在节点在同一路径中之前被访问过的情况下才检测循环。

F B C A E D G

是否包含循环

要实现有向图的 DFS 循环检测,如上面的动画所示,我们需要移除无向图邻接矩阵中的对称性。我们还需要使用一个 recStack 数组来跟踪当前递归路径中已访问的顶点。

示例

Python

class Graph:
    # ......
    def add_edge(self, u, v):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.adj_matrix[u][v] = 1
            self.adj_matrix[v][u] = 1
    # ......
    def dfs_util(self, v, visited, recStack):
        visited[v] = True
        recStack[v] = True
        print("Current vertex:",self.vertex_data[v])

        for i in range(self.size):
            if self.adj_matrix[v][i] == 1:
                if not visited[i]:
                    if self.dfs_util(i, visited, recStack):
                        return True
                elif recStack[i]:
                    return True
        
        recStack[v] = False
        return False

    def is_cyclic(self):
        visited = [False] * self.size
        recStack = [False] * self.size
        for i in range(self.size):
            if not visited[i]:
                print() #new line
                if self.dfs_util(i, visited, recStack):
                    return True
        return False

g = Graph(7)

# ......

g.add_edge(3, 0)  # D -> A
g.add_edge(0, 2)  # A -> C
g.add_edge(2, 1)  # C -> B
g.add_edge(2, 4)  # C -> E
g.add_edge(1, 5)  # B -> F
g.add_edge(4, 0)  # E -> A
g.add_edge(2, 6)  # C -> G

g.print_graph()

print("Graph has cycle:", g.is_cyclic())
运行示例 »

第 6 行:此行已删除,因为它仅适用于无向图。

第 26 行:recStack 数组用于跟踪在递归探索路径过程中访问过的顶点。

第 14-19 行:对于每个之前未访问过的邻接顶点,执行递归 DFS 循环检测。如果邻接顶点之前已经被访问过,并且也在同一递归路径中(第 13 行),则已找到循环,并返回 True


并查集循环检测

使用并查集检测循环与使用深度优先搜索非常不同。

并查集循环检测的工作原理是:首先将每个节点放入自己的子集中(像一个袋子或容器)。然后,对于每条边,合并属于每个顶点的子集。对于一条边,如果两个顶点已经属于同一个子集,则表示我们找到了一个循环。

F E D A C B G

是否包含循环

在上面的动画中,并查集循环检测探索了图中的边。随着边的探索,顶点 A 的子集扩展到包括顶点 B、C 和 D。当探索边 A 和 D 之间的边时,检测到循环,并发现 A 和 D 都已经属于同一个子集。

D、E 和 F 之间的边也构成一个圆圈,但该圆圈未被检测到,因为算法在检测到第一个圆圈时停止(返回 True)。

并查集循环检测仅适用于无向图。

并查集循环检测使用 邻接矩阵表示 实现,因此用顶点和边设置图结构与之前的示例基本相同。

示例

Python

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size
        self.parent = [i for i in range(size)]  # Union-Find array

    def add_edge(self, u, v):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.adj_matrix[u][v] = 1
            self.adj_matrix[v][u] = 1

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

    def find(self, i):
        if self.parent[i] == i:
            return i
        return self.find(self.parent[i])

    def union(self, x, y):
        x_root = self.find(x)
        y_root = self.find(y)
        print('Union:',self.vertex_data[x],'+',self.vertex_data[y])
        self.parent[x_root] = y_root
        print(self.parent,'\n')

    def is_cyclic(self):
        for i in range(self.size):
            for j in range(i + 1, self.size):
                if self.adj_matrix[i][j]:
                    x = self.find(i)
                    y = self.find(j)
                    if x == y:
                        return True
                    self.union(x, y)
        return False

g = Graph(7)

g.add_vertex_data(0, 'A')
g.add_vertex_data(1, 'B')
g.add_vertex_data(2, 'C')
g.add_vertex_data(3, 'D')
g.add_vertex_data(4, 'E')
g.add_vertex_data(5, 'F')
g.add_vertex_data(6, 'G')

g.add_edge(1, 0)  # B - A
g.add_edge(0, 3)  # A - D
g.add_edge(0, 2)  # A - C
g.add_edge(2, 3)  # C - D
g.add_edge(3, 4)  # D - E
g.add_edge(3, 5)  # D - F
g.add_edge(3, 6)  # D - G
g.add_edge(4, 5)  # E - F

print("Graph has cycle:", g.is_cyclic())
运行示例 »

第 6 行:parent 数组包含每个子集的根顶点。这用于通过检查边两侧的两个顶点是否已属于同一个子集来检测循环。

第 17 行:find 方法查找给定顶点所属集合的根。

第 22 行:union 方法合并两个子集。

第 29 行:is_cyclic 方法使用 find 方法来检测循环,如果两个顶点 xy 已经位于同一个子集中。如果未检测到循环,则使用 union 方法合并子集。


DSA 练习

通过练习来测试自己

练习

什么是图中的循环?

A cycle in a Graph is a path 
that starts and ends at the 
same , where no  
are repeated.

开始练习



×

联系销售

如果您想将 W3Schools 服务用于教育机构、团队或企业,请发送电子邮件给我们
sales@w3schools.com

报告错误

如果您想报告错误,或想提出建议,请发送电子邮件给我们
help@w3schools.com

W3Schools 经过优化,旨在方便学习和培训。示例可能经过简化,以提高阅读和学习体验。教程、参考资料和示例会不断审查,以避免错误,但我们无法保证所有内容的完全正确性。使用 W3Schools 即表示您已阅读并接受我们的使用条款Cookie 和隐私政策

版权所有 1999-2024 Refsnes Data。保留所有权利。W3Schools 由 W3.CSS 提供支持