寻找有向图中的最长环:两种解法的深度解析
在算法问题中,寻找有向图中的最长环是一个经典问题。今天,我将分享两种不同的解法:时间戳法和拓扑排序法。这两种方法各有特点,适用于不同场景。
问题描述
给定一个包含 n
个节点的有向图,节点编号为 0
到 n-1
,每个节点最多有一个出边。图的结构由数组 edges
表示,其中 edges[i]
表示节点 i
指向的节点,若 edges[i] = -1
则表示无出边。要求找出图中最长的环,若无环则返回 -1
。
示例1
输入:edges = [3,3,4,2,3]
输出:3
解释:最长环为 2 -> 4 -> 3 -> 2
,长度为 3。
示例2
输入:edges = [2,-1,3,1]
输出:-1
解释:图中无环。
方法一:时间戳法
核心思想
时间戳法利用访问顺序的时间戳来检测环。通过记录每个节点的访问时间,当再次访问到某个节点时,若该节点在当前路径中,则说明形成了一个环。环的长度可以通过时间差计算。
实现步骤
- 初始化:使用全局标记数组
visited
避免重复处理节点。 - 遍历节点:对每个未访问的节点,沿出边遍历,记录路径中的节点及其访问时间。
- 检测环:若遇到当前路径中的已访问节点,计算环长度;否则终止遍历。
- 更新结果:维护最大环长度。
代码实现
class Solution:
def longestCycle(self, edges: List[int]) -> int:
n = len(edges)
visited = [False] * n # 全局访问标记
max_cycle = -1
for i in range(n):
if not visited[i]:
time_map = {} # 当前路径的时间戳记录
current_node = i
time = 0
while True:
if current_node == -1: # 无出边,终止
break
if current_node in time_map: # 发现环
cycle_length = time - time_map[current_node]
max_cycle = max(max_cycle, cycle_length)
break
if visited[current_node]: # 已访问过,但不在当前路径中
break
time_map[current_node] = time # 记录当前节点的时间
visited[current_node] = True # 标记为已访问
current_node = edges[current_node] # 移动到下一个节点
time += 1
return max_cycle if max_cycle != -1 else -1
复杂度分析
- 时间复杂度:O(n),每个节点仅被访问一次。
- 空间复杂度:O(n),存储访问标记和时间戳。
适用场景
时间戳法适用于单出边图的最长环问题,能够高效地检测环并计算其长度。
方法二:拓扑排序法
核心思想
拓扑排序法通过消除图中的非环部分,剩下的节点必然构成环。具体步骤如下:
- 计算入度:统计每个节点的入度。
- 拓扑排序:使用队列处理所有入度为0的节点,逐步消除非环节点。
- 遍历剩余节点:未被访问的节点构成环,计算每个环的长度。
实现步骤
- 计算入度:遍历每个节点,统计每个节点的入度。
- 拓扑排序:将入度为0的节点加入队列,逐步减少其他节点的入度。
- 遍历剩余节点:未被访问的节点属于环,计算每个环的长度。
代码实现
from typing import List
from collections import deque
class Solution:
def longestCycle(self, edges: List[int]) -> int:
n = len(edges)
indegree = [0] * n # 记录每个节点的入度
# 计算每个节点的入度
for u in range(n):
v = edges[u]
if v != -1:
indegree[v] += 1
visited = [False] * n # 标记是否被访问过
q = deque()
# 初始化队列,将入度为0的节点加入队列
for u in range(n):
if indegree[u] == 0:
q.append(u)
# 进行拓扑排序,消除非环节点
while q:
u = q.popleft()
visited[u] = True
v = edges[u]
if v != -1:
indegree[v] -= 1
if indegree[v] == 0:
q.append(v)
max_length = -1 # 记录最长环的长度
# 遍历剩余未被访问的节点,计算每个环的长度
for u in range(n):
if not visited[u]:
count = 0
current = u
while True:
count += 1
visited[current] = True
current = edges[current]
if current == u:
break
if count > max_length:
max_length = count
return max_length if max_length != 0 else -1
复杂度分析
- 时间复杂度:O(n),每个节点仅被访问一次。
- 空间复杂度:O(n),存储入度和访问标记。
适用场景
拓扑排序法适用于单出边图的最长环问题,能够高效地分离出环部分并计算其长度。
两种方法的对比
方法 | 优点 | 缺点 |
---|---|---|
时间戳法 | 实现简单,直接检测环 | 需要额外的时间戳记录 |
拓扑排序法 | 空间效率高,分离非环节点 | 需要两次遍历(拓扑排序和环检测) |
选择建议
- 时间戳法:适用于需要直接检测环的场景,代码实现较为直观。
- 拓扑排序法:适用于需要分离非环节点的场景,适合大规模图的处理。
总结
两种寻找有向图中最长环的方法:时间戳法和拓扑排序法。两种方法各有优缺点,但都能高效解决问题。时间戳法通过记录访问时间直接检测环,而拓扑排序法则通过消除非环节点来间接找到环。