题目描述
你这个学期必须选修 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 <= 10^5
思路
- 可以理解为,是否是有向无环图(DAG)
- 通过拓扑排序判断
- 可以用邻接表(adjacency)降低算法复杂度
方法一:入度表(广度优先算法)
- 根据每个节点设计入度表
indegrees
- 借助队列,将所有入度为0节点入队
- queue不为空,出队,删除此节点课程(已完成该节点拓扑)
- 实际上只是将入度-1。
- 入度-1后邻接节点入度为0,说明前驱已全部删除,将cur入队
- pre出队时,执行numCourses–
- 若为有向无环图,则所有节点一定都入队并出队,完成拓扑排序。
- 当numCourses==0.说明课程可以成功安排。
class Solution {
//有向无环图结构
public boolean canFinish(int numCourses, int[][] prerequisites) {
//计算入度
int[] indegrees = new int[numCourses];
//设计邻接表
List<List<Integer>> adjacency = new ArrayList();
//利于队列结构进行拓扑排序
Queue<Integer> queue = new LinkedList();
//先在邻接表设计各个课程的节点
for(int i=0;i<numCourses;i++){
adjacency.add(new ArrayList());
}
//计算入度及邻接情况
for(int[] cp:prerequisites){
indegrees[cp[0]]++;
adjacency.get(cp[1]).add(cp[0]);
}
//逐个课程找出入度为0作为拓扑起点
for(int i=0; i<numCourses; i++){
if(indegrees[i]==0){
queue.add(i);
}
}
//取出拓扑起点,统计课程数-1,将邻接节点的入度-1,判断该节点是否满足拓扑排序,满足则加入队列
while(!queue.isEmpty()){
int cur = queue.poll();
numCourses--;
for(int tmp:adjacency.get(cur)){
if(--indegrees[tmp]==0) queue.add(tmp);
}
}
//若满足拓扑排序,则所有节点都经过入队出队。
return numCourses==0;
}
}
方法二:深度优先算法
判断图中是否有环
- 借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
未被 DFS 访问:i == 0;
已被其他节点启动的 DFS 访问:i == -1;
已被当前节点启动的 DFS 访问:i == 1。 - 对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
- 终止条件:
当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2次访问,即 课程安排图有环 ,直接返回 False。 - 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
- 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
- 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1并返回 True。
- 若整个图 DFS 结束并未发现环,返回 True。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adjacency = new ArrayList();
for(int i=0;i<numCourses;i++){
adjacency.add(new ArrayList());
}
//利用标记找环
int[] flags = new int[numCourses];
for(int[] cp:prerequisites){
adjacency.get(cp[1]).add(cp[0]);
}
//深度搜索,若出现false,则整体false
for(int i=0; i<numCourses; i++){
if(!dfs(flags,i,adjacency)) return false;
}
return true;
}
public boolean dfs(int[] flags,int i, List<List<Integer>> adjacency){
//终止条件,若访问的为1,则说明出现环,返回false,为-1则停止当前递归。(剪枝)
if(flags[i]==1) return false;
if(flags[i]==-1) return true;
//将当前节点置为1
flags[i]=1;
//逐个访问邻接节点,进行深搜
for(Integer cur:adjacency.get(i)){
if(!dfs(flags,cur,adjacency)) return false;
}
//回溯,用-1表示当前及后面节点已经访问过。
flags[i]=-1;
return true;
}
}