题目介绍
现在你总共有 n 门课需要选,记为 0 到 n-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。这是不可能的。
说明:
- 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
提示:
- 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
- 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
- 拓扑排序也可以通过 BFS 完成。
解题思路
-
根据提示已经可以很清楚地知道此问题是检测是否存在拓扑排序的问题,换言之也是检测有向图中是否存在环的问题。
-
课程与课程的关系可以用有向图来表示,那么如何数据结构的方式表示有向图呢?
答:说到图(想到了邻接表了),下面的代码中我采用了HashMap哈希表来表示,当然也可以用其他表示。 -
有了图就可以愉快地遍历了,当然我们这里要以“拓扑排序”的方式遍历。
- 就是每次都以入度为0的顶点开始入手,“移除”这个顶点,然后找寻下一个入度为0的顶点,以此下去。
- 最后判断入度数组中(用一维数组表示,数组下标为顶点,数组值为每个顶点对应的入度)是否存在有不为0的顶点,有则表明不存在拓扑排序,反之亦然。
- 上述过程可采用BFS来实行,BFS遍历完后就相当于进行了一次拓扑排序了。
- 还有一种方式是检测方式是采用DFS,这种方式主要是检测要走的下一个顶点是否之前走过,若走过则证明图中存在环,即不存在拓扑排序。
实现代码
BFS拓扑排序
public static boolean canFinish(int numCourses, int[][] prerequisites) {
//有向图map,第一个数字为起点,第二个list存储了该起点能到达的其他点
Map<Integer, List<Integer>> map = new HashMap<>();
//每个顶点的入度,初始化为0
int[] degree = new int[numCourses];
Arrays.fill(degree, 0);
//初始化
for(int[] a:prerequisites) {
if(map.containsKey(a[1])) {
map.get(a[1]).add(a[0]);
}
else {
List<Integer> list = new ArrayList<Integer>();
list.add(a[0]);
map.put(a[1], list);
}
degree[a[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
//将入度为0的点提前加入队列
for(int i=0;i < numCourses;i++) {
if(degree[i]==0) {
queue.offer(i);
}
}
//BFS过程采用了队列这种数据结构
while(!queue.isEmpty()) {
//取出v,即度为0的点
int v = queue.poll();
//对v所连接的点进行度-1的操作
List<Integer> list = map.get(v);
if(list == null)
continue;
for(int j = 0;j < list.size();j++){
int o_v = list.get(j);
degree[o_v]--;
//判断度减一后的该点是否符合要求
if(degree[o_v]==0)
queue.offer(o_v);
}
}
//如果上述操作执行完后还有度不为0的点,则证明有向图中存在环,不满足拓扑排序
for(int k=0;k < numCourses;k++) {
if(degree[k]!=0)
return false;
}
//上述操作没问题后,返回true
return true;
}
DFS拓扑排序
public static boolean canFinish(int numCourses, int[][] prerequisites) {
//path记录
boolean[] path = new boolean[numCourses];
//onepath记录从某个结点出发所经过的结点
boolean[] onepath = new boolean[numCourses];
Arrays.fill(onepath, false);
Arrays.fill(path, false);
//有向图map,第一个数字为起点,第二个list存储了该起点能到达的其他点
Map<Integer, List<Integer>> map = new HashMap<>();
//初始化
for(int[] a:prerequisites) {
if(map.containsKey(a[1])) {
map.get(a[1]).add(a[0]);
}
else {
List<Integer> list = new ArrayList<Integer>();
list.add(a[0]);
map.put(a[1], list);
}
}
//DFS过程
for (int i = 0; i < numCourses; i++) {
//避免重复
if(path[i]) continue;
//若找到有环则返回false
if(dfs_find_circle(map, i, path, onepath))
return false;
}
return true;
}
public static boolean dfs_find_circle(Map<Integer, List<Integer>> map,int v,boolean[] path,boolean[] onepath) {
/*只要是这趟路径下来的所经过的点(即一个for循环),都会被onepath标记,
假如有顶点下个点是之前走过的,证明有环*/
if (onepath[v]) return true;
//对这两数组标记
onepath[v] = path[v] = true;
//对点v相连接的点进行dfs
List<Integer> list = map.get(v);
if(list!=null) {
for(int i=0;i < list.size();i++) {
int o_v = list.get(i);
if(dfs_find_circle(map, o_v, path, onepath))
return true;
}
}
//可证明这条路径不存在闭环,则去除这个路径上的这个点
onepath[v] = false;
return false;
}
总结归纳
- 这道题主要考察了拓扑排序,反正我是这么记着的:(每次找点都找入度为0的顶点)找点,移点,找点,移点…
- 在用DFS和BFS两种不同的方式实现拓扑排序,再次熟悉了DFS和BFS的两种搜索的过程,前者先纵后横,后者先横后纵。
- 通过图画的圈圈和箭头线的方式更容易理解上面两种搜索过程!