LeetCode 1462. 课程表 IV
题目描述



题意分析
每条先修关系
[a, b]表示学习课程b前必须先学课程a,即有向边a → b。对于每个查询[u, v],判断u是否是v的先修课,既包含直接关系,也包含经过其他课程传递得到的间接关系。问题不是返回某个可行学习顺序。两门没有依赖的课也可以在某个顺序中一前一后,但这不代表前者是后者的先修课;必须确实存在从
u到v的有向路径。题目保证依赖图无环,各查询按输入顺序分别返回布尔结果。
解法:传递闭包
核心思路
[!blue]
查询数量较多,而课程数较小,可以先计算任意两门课之间是否可达,再把每次查询变成一次查表。定义
reach[i][j]表示存在从课程i到课程j的先修路径,初始只写入给出的直接边。逐个允许课程作为路径的中转点。开始第
k轮前,表中记录的是只允许编号小于k的课程出现在路径中间时的可达性。加入中转点k后,一条路径要么不经过它,沿用旧状态;要么经过它,拆成i → k和k → j两段。只要这两段都存在,就能确认
i → j,所以转移是把reach[i][k] && reach[k][j]与旧reach[i][j]做逻辑或。更新只增加可达关系,不能清除之前通过其他中转点已经找到的路径。中转点必须放在最外层,才能在一轮结束后统一得到“允许前
k + 1个中转点”的完整结果。若只把k放到最内层扫一遍,某条所需的间接路径可能还没计算出来,后面变成可达时却不再回头更新原查询对。代码在
i不能到达当前k时跳过整段内循环,因为此时不存在经过k的新路径。题目无环,对角线不需要设真,课程也不会通过非空路径回到自己。所有中转点处理完后,表中就包含全部直接与间接先修关系。
解题步骤
- 创建全为假的
n × n可达表,按[先修课, 后续课]的方向写入直接边。- 最外层依次枚举中转点
k。- 枚举起点
i,若i无法到达k,跳过这个起点的本轮扩展。- 否则枚举终点
j,当k能到达j时,将reach[i][j]置真。- 处理完全部中转点后,按原顺序返回每个查询对应的表项。
代码实现
class Solution {
public List<Boolean> checkIfPrerequisite(int n, int[][] prerequisites, int[][] queries) {
// reach[i][j]:i 到 j 是否存在有向路径。对角线保持 false,自己不是自己的先修课。
boolean[][] reach = new boolean[n][n];
for (int[] p : prerequisites) {
reach[p[0]][p[1]] = true;
}
// Floyd 求传递闭包:中转点 k 必须在最外层,否则归纳前提被破坏。
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
// i 到不了 k,经过 k 的路径全都不存在,整层内层循环可跳过。
if (!reach[i][k]) {
continue;
}
for (int j = 0; j < n; j++) {
if (reach[k][j]) {
reach[i][j] = true;
}
}
}
}
List<Boolean> res = new ArrayList<>(queries.length);
for (int[] q : queries) {
res.add(reach[q[0]][q[1]]);
}
return res;
}
}
func checkIfPrerequisite(n int, prerequisites [][]int, queries [][]int) []bool {
// reach[i][j]:i 到 j 是否存在有向路径。对角线保持 false,自己不是自己的先修课。
reach := make([][]bool, n)
for i := 0; i < n; i++ {
reach[i] = make([]bool, n)
}
for _, p := range prerequisites {
reach[p[0]][p[1]] = true
}
// Floyd 求传递闭包:中转点 k 必须在最外层,否则归纳前提被破坏。
for k := 0; k < n; k++ {
for i := 0; i < n; i++ {
// i 到不了 k,经过 k 的路径全都不存在,整层内层循环可跳过。
if !reach[i][k] {
continue
}
for j := 0; j < n; j++ {
if reach[k][j] {
reach[i][j] = true
}
}
}
}
res := make([]bool, len(queries))
for i, q := range queries {
res[i] = reach[q[0]][q[1]]
}
return res
}
复杂度分析
- 时间复杂度:
O(n³ + e + q)。写入e条直接边,三层循环计算传递闭包,最后处理q个常数时间查询。- 空间复杂度:可达表占
O(n²),返回的查询结果另占O(q)。
关键点总结
[!green]
- 先修关系是有方向的可达性,不是拓扑排序中的偶然先后。
- 每轮增加一个可用中转点,给三层循环提供明确的阶段不变量。
- 新路径与旧路径取并集,已有可达关系始终保留。
- 一次预处理服务全部查询,查询阶段不再重复搜索。
易错点总结
[!yellow]
- 把输入边方向写反:本题
[a, b]表示a是b的先修,而不是相反。- 只检查直接关系表:会漏掉经过多门课传递的依赖。
- 用拓扑排名比较代替可达性:排名靠前不代表存在先修路径。
- 中转点放最内层且只扫描一次:依赖关系可能在使用之后才被算出,造成间接路径遗漏。
- 用本轮两段条件覆盖旧值:当前中转不可用不代表两点原本不可达,必须保留旧的真值。
- 把边当成无向连接:反向到达通常不成立,不能对称写入。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 210. 课程表 II | 中等 | 拓扑顺序只能说明某种合法先后,u排在v前不代表存在先修路径,本题必须维护传递可达性。 |
| 207. 课程表 | 中等 | 原题只判整个依赖图是否无环,本题对很多指定节点对回答真实依赖关系。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!