课程表这题,真正卡人的不是建图,是你怎么判断“能不能学完”
面试里一提到“课程表”,很多人第一反应是拓扑排序。方向没错,但这题如果只背模板,写起来还是容易乱。因为它表面上是排课,实际上问的是:这堆先修关系里有没有环。
题目意思不复杂。比如要学课程 1,前提是先学 0,那就记成一条边:0 -> 1。如果最后图里出现了这种关系:
0 -> 1 -> 2 -> 0
那就没法学完了。因为谁都在等前一门课,绕成了一圈。
我平时更习惯用 入度 + 队列 来做,代码直一点,也比较适合 PHP 这种偏业务语言来写。
先把图建出来。
<?phpfunctioncanFinish(int $numCourses, array $prerequisites): bool{ $graph = array_fill(0, $numCourses, []); $indegree = array_fill(0, $numCourses, 0);foreach ($prerequisites as $edge) { $course = $edge[0]; $pre = $edge[1]; $graph[$pre][] = $course; $indegree[$course]++; } $queue = [];for ($i = 0; $i < $numCourses; $i++) {if ($indegree[$i] === 0) { $queue[] = $i; } } $learned = 0; $head = 0;while ($head < count($queue)) { $cur = $queue[$head++]; $learned++;foreach ($graph[$cur] as $next) { $indegree[$next]--;if ($indegree[$next] === 0) { $queue[] = $next; } } }return $learned === $numCourses;}
这段代码核心就两步。
第一步,统计每门课的入度。谁的入度是 0,说明它现在不用等别人,可以先学。
第二步,学掉这门课以后,把它指向的后续课程入度减 1。谁被减到 0 了,谁再进队列。
最后如果学到的课程数等于总课程数,说明整张图被顺利“拆完”了,没有环;反过来,如果还有课程永远进不了队列,那基本就是被环卡住了。
拿这个例子跑一下会比较清楚:
$numCourses = 4;$prerequisites = [ [1, 0], [2, 1], [3, 2]];var_dump(canFinish($numCourses, $prerequisites)); // true
这里就是 0 -> 1 -> 2 -> 3,能一路学完。
再看一个不能学完的:
$numCourses = 3;$prerequisites = [ [1, 0], [2, 1], [0, 2]];var_dump(canFinish($numCourses, $prerequisites)); // false
这组数据里,0、1、2 谁都依赖别人,初始时没有一门课入度为 0,队列一开始就是空的。代码跑到最后,$learned 一定小于总课程数。
这题写的时候有个细节挺容易反。[a, b] 不是 a -> b,而是 学 a 之前要先学 b,所以边应该建成 b -> a。这个地方一反,后面拓扑排序逻辑全是对的,结果也会错。
当然,这题也能用 DFS 做。DFS 的思路更像是在图里“找环”,把节点状态分成 3 种:
如果递归过程中又走到了状态为 1 的节点,那就是回到了当前路径上的点,环就找到了。
我放一个精简版:
<?phpfunctioncanFinishByDfs(int $numCourses, array $prerequisites): bool{ $graph = array_fill(0, $numCourses, []);foreach ($prerequisites as $edge) { $graph[$edge[1]][] = $edge[0]; } $state = array_fill(0, $numCourses, 0); $dfs = function($node)use(&$dfs, &$graph, &$state): bool{if ($state[$node] === 1) returnfalse;if ($state[$node] === 2) returntrue; $state[$node] = 1;foreach ($graph[$node] as $next) {if (!$dfs($next)) returnfalse; } $state[$node] = 2;returntrue; };for ($i = 0; $i < $numCourses; $i++) {if (!$dfs($i)) returnfalse; }returntrue;}
不过真到面试里,我还是建议优先写 BFS 拓扑排序。一个原因是过程更可视化,另一个原因是“入度为 0 的先学”这件事,跟题意天然贴合,不容易把自己绕进去。
课程表这题本身不难,难点主要有两个:一是边的方向别写反,二是别把拓扑排序写成普通遍历。普通遍历只是在走图,这题真正要盯的是:最后到底有没有课程一直卡着,始终进不了队列。这个量一统计,题就落下来了。