题目:
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例
- 示例 1
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
- 示例 2
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
提示
- 输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵
- 可以假定输入的先决条件中没有重复的边。
- 1 <= numCourses <= 1 0 5 10^5 105
抛砖引玉
思路
- 之前考虑prerequisites是依赖关系的集合可能包含多个子集,
- 这样一个prerequisites子集中不相邻的子集即[1,0,2]中1,2的出度入度就不好统计了
看了官方的题解,prerequisites多了限制条件只有两个元素,那统计出度入度就简便了:
- 对numCourses中任意一门课其包含依赖它的(入度),其依赖的(出度)
- 统计每个元素入度数量及出度子集
- 当一个元素的入度数量为0时,则说明可以选择这个元素作为入口,即选修它
- 遍历(选修)其出度子集,子集入度均减一,即枚举选修他们
- 遇到入度为0,则继续上面的逻辑,知道枚举出最后一个入度数量为0元素,每尝试一次枚举即表示选修了一门课
- 如果所有的课都选修了则返回true
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function (numCourses, prerequisites) {
let mapItem = new Map(), // 出度子集
mapNum = new Map(), // 入度连接数
_result = numCourses,
startList = [],
len = prerequisites.length;
// 初始化入度连接数 与 出度子集
for (let i = 0; i < numCourses; i++) {
mapItem.set(i, []);
mapNum.set(i, 0);
}
// 填充入度、出度数据
for (let i = 0; i < len; i++) {
// prerequisites[1] 需要在 prerequisites[0] 之前
// before -> after
// before(出度) -> after (入度)
let after = prerequisites[i][0],
before = prerequisites[i][1],
afterValue = mapNum.get(after),
beforeValue = mapItem.get(before);
mapItem.set(before, [...beforeValue, after])
mapNum.set(after, afterValue + 1)
}
// 取出不依赖其他课的元素
for (let [key, value] of mapNum) {
if (value === 0) startList.push(key);
}
while (startList.length) {
// 枚举选修不依赖未学习的课的元素
let item = startList.shift(),
nextItem = mapItem.get(item);
// 枚举一轮带选课减一
_result--;
if (nextItem && nextItem.length) {
// 尝试本轮所有选课可能,遇到选择后存在入度为0的元素,
// 则推进枚举list,之后尝试按照这种路线选课
for (let i = 0; i < nextItem.length; i++) {
let nextNum = mapNum.get(nextItem[i]) - 1;
mapNum.set(nextItem[i], nextNum);
if (nextNum === 0) {
startList.push(nextItem[i]);
}
}
}
}
// 是否选择了所有课程
return _result === 0;
}
深度优先搜索
- 记录每个课程的前置课程
- 遍历选择课程,选择一门课标记1, 再遍历其依赖的课程:
- 如果前置课程未被选择,则转换成选择这门前置课程
- 如果前置课程存在可以被选择且不再有依赖的课程则标记2
- 如果前置课程同样被标记1则说明:
该门课的前置课程同样存在无法满足无依赖的前置课程及形成了无入度的闭环
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function(numCourses, prerequisites) {
let map = new Map(),
valid = true,
visited = Array(numCourses).fill(0);
for (let i = 0; i < numCourses; i++) {
map.set(i, [])
}
// 查询每个课程的的前置课程 即可能的出度子集
for (let i = 0; i < prerequisites.length; i++) {
let after = prerequisites[i][0],
before = prerequisites[i][1],
beforeValue = map.get(before);
map.set(before, [...beforeValue, after])
}
// 循环为被学习的课程检修是否存在 闭环
for (let i = 0; (i < numCourses) && valid; i++) {
if (visited[i] == 0) {
dfs(i);
}
}
// 输入的课程标记选择1
function dfs(u) {
visited[u] = 1;
for (let v of map.values) {
// 如果前置课程未被选择过则先选择前置课程
if (visited[v] === 0) {
dfs(v);
if (!valid) {
return;
}
} else if (visited[v] === 1) {
// 如果前置元素同样被标记1,则说明在找u的前置课程v是v的前置课程也同样依赖未选修的前置课程
valid = false;
return;
}
}
// 完成前置课程查询标记该课程完成2
visited[u] = 2;
}
// 默认成功,存在闭环则返回失败
return valid;
};
博客: 小书童博客
每天的每日一题,写的题解会同步更新到公众号一天一大 lee 栏目
欢迎关注留言
公号: 坑人的小书童