Skip to content

题目链接 · 灵神原题解(署名来源)

题意

给你一个有向图,判断图中是否有环。

核心思路

如果在递归过程中,发现下一个节点在递归栈中(正在访问中),则找到了环。

lc207.svg

如上图,我们 DFS 访问 03453。走到 5 的时候,发现下一个节点 3 在递归栈中(正在访问中),那么就找到了环。

注:说节点 x「正在访问中」,是说我们正在递归处理节点 x 的邻居,dfs(x) 尚未结束。

具体思路

对于每个节点 x,都定义三种颜色值(状态值):

0:节点 x 尚未被访问到。 1:节点 x 正在访问中,dfs(x) 尚未结束。 2:节点 x 已经完全访问完毕。注意这还说明从 x 出发无法找到环。所以当我们遇到状态值为 2 的节点 x 时,无需递归 x

误区:不能只用两种状态表示节点「没有访问过」和「访问过」。如上图,我们先 DFS 访问 012,再访问 02,此时 0 的邻居 2 已经访问过,但这并不能表示此时就找到了环。

算法流程:

  1. 建图:把每个 prerequisites[i]=[a,b] 看成一条有向边 ba,构建一个有向图 g
  2. 创建长为 numCourses 的颜色数组 colors,所有元素值初始化成 0
  3. 遍历 colors,如果 colors[i]=0,则调用递归函数 dfs(i)
  4. 执行 dfs(x)
    1. 首先标记 colors[x]=1,表示节点 x 正在访问中。
    2. 然后遍历 x 的邻居 y。如果 colors[y]=1,则找到环,返回 true。如果 colors[y]=0(没有访问过)且 dfs(y) 返回了 true,那么 dfs(x) 也返回 true
    3. 如果没有找到环,那么先标记 colors[x]=2,表示 x 已经完全访问完毕,然后返回 false
  5. 如果 dfs(i) 返回 true,那么找到了环,返回 false
  6. 如果遍历完所有节点也没有找到环,返回 true
python
class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        g = [[] for _ in range(numCourses)]
        for a, b in prerequisites:
            g[b].append(a)

        colors = [0] * numCourses
        # 返回 True 表示找到了环
        def dfs(x: int) -> bool:
            colors[x] = 1  # x 正在访问中
            for y in g[x]:
                # 情况一:colors[y] == 1,表示发生循环依赖,找到了环
                # 情况二:colors[y] == 0,没有访问过 y,继续递归 y 获取信息
                # 情况三:colors[y] == 2,重复访问 y 只会重蹈覆辙,和之前一样无法找到环,跳过
                if colors[y] == 1 or colors[y] == 0 and dfs(y):
                    return True  # 找到了环
            colors[x] = 2  # x 完全访问完毕,从 x 出发无法找到环
            return False  # 没有找到环

        for i, c in enumerate(colors):
            if c == 0 and dfs(i):
                return False  # 有环
        return True  # 没有环
cpp
// C++ 版待补充
cpp
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<vector<int>> g(numCourses);
        for (auto& p : prerequisites) {
            g[p[1]].push_back(p[0]);
        }

        vector<int> colors(numCourses);
        // 返回 true 表示找到了环
        auto dfs = [&](this auto&& dfs, int x) -> bool {
            colors[x] = 1; // x 正在访问中
            for (int y : g[x]) {
                // 情况一:colors[y] == 1,表示发生循环依赖,找到了环
                // 情况二:colors[y] == 0,没有访问过 y,继续递归 y 获取信息
                // 情况三:colors[y] == 2,重复访问 y 只会重蹈覆辙,和之前一样无法找到环,跳过
                if (colors[y] == 1 || colors[y] == 0 && dfs(y)) {
                    return true; // 找到了环
                }
            }
            colors[x] = 2; // x 完全访问完毕,从 x 出发无法找到环
            return false; // 没有找到环
        };

        for (int i = 0; i < numCourses; i++) {
            if (colors[i] == 0 && dfs(i)) {
                return false; // 有环
            }
        }
        return true; // 没有环
    }
};

复杂度分析

  • 时间复杂度:O(n+m),其中 nnumCoursesmprerequisites 的长度。每个节点至多递归访问一次,每条边至多遍历一次。
  • 空间复杂度:O(n+m)。存储 g 需要 O(n+m) 的空间。

相似题目

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

本文整理自灵茶山艾府(endlesscheng)的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传