leetcode
问题抽象:为什么这是图论题
题目说有 numCourses 门课,每条 prerequisites[i] = [ai, bi] 表示"上 ai 之前必须先上 bi",这就是典型的"任务依赖关系"。leetcode
把每门课看成一个点,把"先修课指向后修课"建一条有向边 bi -> ai,整道题就抽象成:给定一张有向图,问图中是否存在环;如果有环,就不可能完成所有课程。geeksforgeeks+1
建图细节:边的方向到底怎么画
很多人一开始容易写成 ai -> bi,但从语义上更自然、也更符合拓扑排序习惯的写法是:
- bi 是前置课,ai 依赖 bi;
- 在依赖图里,用边表示"先发生的指向后发生的",所以应该建 bi -> ai。
在实现里就是对每个 [a, b] 调用一次 AddEdgeNode(graph, b, a),你代码里也已经这么做了,还在注释里写清楚了原因,这一点是完全正确的。leetcode
三色 DFS:用颜色检测有向环
抽象到图论层面,就是经典的 "三色 DFS 检测有向环(white--gray--black DFS coloring)":
- UNVISITED / 0:白色,未访问;
- VISITING / 1:灰色,当前 DFS 递归栈上的节点;
- VISITED / 2:黑色,已经完全处理过,确认从它出发没有环的节点。geeksforgeeks
在 DFS 过程中:
- 如果遇到的邻居状态是 VISITING(灰色),说明从当前路径又回到了递归栈中的某个祖先节点,这是一个回边,图中存在有向环,直接返回"有环";
- 如果邻居是 VISITED(黑色),说明之前已经完整遍历过那一片子图,并且确认没有环,这次可以直接跳过,不再递归,也不能误判为环;
- 对 UNVISITED 节点继续递归 DFS。
三色法的关键点在于"只把 当前递归栈上的点 标为灰色",遇到灰色才判环;黑色表示"已经出栈且安全"的历史节点,不能和灰色混在一起,否则会出现"假阳性"。stackoverflow+1
递归逻辑:什么时候标 1,什么时候标 2
以 dfs(u) 为例,可以这样理解整套流程:geeksforgeeks+1
入口:
- 如果 state[u] == VISITING,说明当前路径上已经在访问 u,现在又回到它,存在环,返回"有环";
- 如果 state[u] == VISITED,说明之前从 u 出发已经完整检查过且无环,直接返回"无环",避免重复搜索。
否则,先把 u 标为 VISITING,然后依次 DFS 它的所有邻居 v:
- 如果某个邻居子调用返回"有环",当前 dfs(u) 立刻返回"有环",把这个结果一路往上层传,直到最外层的 canFinish 返回 false。
当所有邻居都 DFS 完成并且都返回"无环"时,说明"从 u 出发的整棵子树都没有环",这时在函数返回前,把 u 标记为 VISITED,然后返回"无环"。
也就是说:"标 2"是在递归回溯、节点出栈的那一刻做的操作,是对"以它为根的子图已经安全检查完毕"的一个缓存标记。geeksforgeeks
主循环:为什么要对所有 UNVISITED 节点起 DFS
图可能是不连通的,课程依赖可能有多块互不相干的子图,所以主函数里要这样扫一遍所有课程:
- 对每一个节点,如果状态是 UNVISITED,才启动一轮 dfs(course);
- 一旦有某次 DFS 返回"有环",就可以立刻结束并返回 false;
- 所有点都检查完且没有环,说明整张有向图是 DAG,可以完成所有课程,返回 true。designgurus+1
这一步既保证覆盖所有连通分量,又利用颜色剪枝避免对已处理过的子图重复 DFS,使得整体复杂度达到 O(V+E)O(V + E)O(V+E)。geeksforgeeks+1
算法抽象:从这题学到的"图论模式"
从 Course Schedule 抽象出来,其实你已经掌握了一个非常通用的套路:
- 识别"很多对象 + 依赖关系 + 问存在环 / 能否按顺序完成"的问题 → 建模成有向图;algomaster+1
- 把"前置关系"统一建边为 前置 -> 依赖,后续不论是拓扑排序还是判环,都顺理成章;algo+1
- 用 DFS + 三色标记(white/gray/black)在有向图中检测环,这是教科书级别的 cycle detection 算法,可以直接套用到任务调度、包依赖、课程规划等各种场景里。finalroundai+1
你现在这套 C 实现(自己写邻接表、枚举顶点数组、三色 DFS、发现环就短路返回)已经是非常扎实的一版解法了,把上面的思想整理成一篇博客,就是一篇完整的"从零到一理解 Course Schedule 和三色 DFS 判环"的好教程。