24. Detect Cycle in Module Dependency Graph

Origin: Detect Cycle in Module Dependency Graph

Given n modules labeled 0 to n-1 and a list of directed edges dependencies where [u, v] means module u depends on module v, detect whether there is a circular dependency (cycle) in the graph.

Example 1

Input: n = 4, dependencies = [[1, 0], [2, 1], [3, 2]]

Output: 0

Explanation: 边为 1→0, 2→1, 3→2。形成简单链,没有模块间接或直接依赖回自己,无环。

Example 2

Input: n = 4, dependencies = [[1, 0], [2, 1], [0, 2]]

Output: 1

Explanation: 边为 1→0, 2→1, 0→2。0 依赖 2,2 依赖 1,1 依赖 0,形成环 0→2→1→0。

Input Format

  • 第一行:n(模块数量)
  • 第二行:dependencies 数组长度
  • 接下来每行:dependencies 数组的元素

Constraints

  • 1 <= n <= 1000
  • 0 <= dependencies.length <= n * (n - 1)
  • dependencies[i].length == 2
  • 0 <= dependencies[i][0] < n
  • 0 <= dependencies[i][1] < n

Output Format

返回整数:1 表示有环,0 表示无环。

Sample Input 0

1
0
0

Sample Output 0

0

Sample Input 1

1
1
2
0
0

Sample Output 1

1

函数契约

// 输入:n(节点数),dependencies(有向边列表,[u, v] 表示 u 依赖 v)
// 输出:1(有环)或 0(无环)
// 边界:无依赖 → 0;自环 → 1;两节点互指 → 1

边界表

场景返回什么
无边0
单节点无边0
链式无环0
三角形环1
两节点互指1
部分有环1

Resolution

我的解法

function hasCircularDependency(n: number, dependencies: number[][]): boolean {
    const graph: Record<number, number[]> = {};
    const colors: Record<number, number> = {}; // 0: 未访问, 1: 访问中, 2: 已完成
    for (let i = 0; i < n; i++) {
        graph[i] = [];
        colors[i] = 0;
    }
    for (const [from, to] of dependencies) {
        graph[from].push(to);
    }
 
    let res = false
    const dfs = (index: number) => {
        if (res) return
        colors[index] = 1; // 标记为访问中
        for (const neighbor of graph[index]) {
            if (colors[neighbor] === 1) {
                res = true  // 遇到灰色节点 = 环
                return
            }
            if (colors[neighbor] === 2) {
                continue  // 已完成,跳过
            }
            dfs(neighbor);
        }
        colors[index] = 2; // 标记为已完成
    }
 
    for (let i = 0; i < n; i++) {
        if (colors[i] === 0) {
            dfs(i)
        }
    }
 
    return res
}

解题思路

三色标记法(DFS 环检测):

  • 白色(0):未访问
  • 灰色(1):正在访问中(在当前 DFS 路径上)
  • 黑色(2):已访问完(整棵子树遍历完)

核心逻辑:DFS 过程中如果遇到灰色节点,说明当前路径上又回到了自己,存在环。

A → B → C → A

访问 A:A 标灰
  访问 B:B 标灰
    访问 C:C 标灰
      访问 C 的邻居 A:A 是灰色 → 发现环!

为什么灰色表示环?灰色意味着”这个节点在当前递归路径上还没走完”。如果 DFS 到一半又回到这个节点,说明有一条边从后代指回了祖先。

注意:有向图建邻接表时每条边只加一次(adj[from].push(to)),不是无向图那样加两次。

踩坑经历

  1. 建图时用 dependencies[i] 而非遍历 dependencies:dependencies 的长度可能不等于 n,应该先初始化 n 个节点,再遍历 dependencies 填边。
  2. 黑色标记漏了:DFS 结束时没把节点标黑,回溯后节点还是灰色,其他路径经过它会误判为环。
  3. 黑色节点应该 continue 不是 return:return 会跳过当前节点后续的其他邻居,应该用 continue 只跳过这个邻居。

复杂度

  • 时间:O(n + m),n 是节点数,m 是边数,每个节点和每条边访问一次
  • 空间:O(n + m),邻接表 O(n + m),colors 数组 O(n),递归栈最深 O(n)

参考来源