问题分析
给定一个关系(relations),每个关系中,第一个元素表示目标节点,后面的元素表示依赖于目标节点的节点。我们要判断这些关系是否构成一个有向图的环路。如果有环,则返回 False;如果没有环,则返回 True。
解题思路
这个问题可以转化为一个图的拓扑排序问题。拓扑排序要求图中不存在环路。如果图中存在环,则无法进行有效的拓扑排序。因此,可以通过以下步骤判断图是否存在环路:
-
图的构建:我们将关系中的每个目标节点作为图中的节点,将每个依赖关系转化为有向边。
-
DFS 检测环:我们通过深度优先搜索(DFS)来检查图中是否存在环。DFS 的过程如下:
- 使用一个
visited集合标记所有已经完全遍历过的节点。 - 使用一个
visiting集合标记当前正在访问的节点。如果在当前路径中再次遇到正在访问的节点,说明图中存在环,返回False。 - 如果当前节点没有环,继续遍历其邻居节点。
- 使用一个
-
判断有无环:如果在图的遍历过程中没有发现环,说明图中是有向无环图(DAG),可以返回
True。
代码详解
pythonCopy Code
from collections import defaultdict
def solution(relations: list[list[str]]) -> bool:
# 构建图的邻接表
graph = defaultdict(list)
for relation in relations:
target = relation[0] # 目标节点
for dependency in relation[1:]: # 依赖节点
graph[dependency].append(target) # 依赖节点指向目标节点
# 记录节点的访问状态
visited = set() # 存储已经完成遍历的节点
visiting = set() # 存储当前正在遍历的节点
# 定义 DFS 函数来检查环
def dfs(node):
# 如果当前节点已经在访问路径中,说明存在环
if node in visiting:
return False
# 如果当前节点已经访问过,直接返回 True
if node in visited:
return True
# 标记当前节点为正在访问
visiting.add(node)
# 遍历所有依赖节点,递归访问
for neighbor in graph[node]:
if not dfs(neighbor):
return False
# 标记当前节点为已访问
visiting.remove(node)
visited.add(node)
return True
# 对图中每个节点进行 DFS 遍历
for node in list(graph.keys()):
if not dfs(node): # 如果存在环,则返回 False
return False
return True # 如果没有环,则返回 True
if __name__ == "__main__":
# 测试用例
print(solution([["A", "B", "C"], ["B", "D"],
["C", "E"], ["D", "A"]]) == False) # 有环,返回 False
print(
solution(
[
["A", "B", "C", "D", "E"],
["F", "G", "H", "I"],
["J", "K", "L", "M", "A"],
["N", "O", "P", "Q"],
["E", "H", "I", "J"],
["R", "S", "T", "U"],
["V", "W", "X"],
["Y", "Z"],
]
)
== False # 有环,返回 False
)
代码解释
-
图的构建:
- 使用
defaultdict(list)创建一个图的邻接表。 - 每一条关系中,第一个元素是目标节点,后面的元素是依赖目标节点的节点。
- 通过
graph[dependency].append(target)将依赖节点指向目标节点。
- 使用
-
DFS 检测环:
-
使用两个集合
visited和visiting来记录节点的状态。visited存储已经遍历过且没有环的节点。visiting存储当前正在递归访问的节点。
-
如果当前节点已经在
visiting集合中,说明存在环,返回False。 -
如果当前节点已经在
visited集合中,直接跳过(避免重复计算)。 -
递归地遍历当前节点的所有邻居节点。如果在遍历过程中发现环,立即返回
False。 -
如果遍历完成且没有环,当前节点标记为已访问。
-
-
主函数流程:
- 遍历图中所有节点,对每个节点进行 DFS 检查。
- 如果有任何一个节点的 DFS 返回
False,则图中存在环,返回False。 - 如果遍历完所有节点后没有发现环,则返回
True,表示图中是有向无环图。
图解
考虑第一个测试用例:
pythonCopy Code
[["A", "B", "C"], ["B", "D"], ["C", "E"], ["D", "A"]]
A是目标节点,B,C是依赖节点。表示B,C依赖A。B是目标节点,D是依赖节点。表示D依赖B。C是目标节点,E是依赖节点。表示E依赖C。D是目标节点,A是依赖节点。表示A依赖D。
该图形成了一个环:A -> B -> D -> A,因此返回 False。
时间复杂度
- 每个节点和每条边最多访问一次,因此时间复杂度是 O(V + E) ,其中
V是图中的节点数,E是边的数目。
空间复杂度
- 空间复杂度主要由图的邻接表和 DFS 的递归栈决定。图的邻接表空间复杂度是 O(V + E) ,DFS 递归栈的空间复杂度最坏为 O(V) 。