There are a total of n courses you have to take, labeled from 0
to n-1
.
Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]
Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?
首先可以考虑一下什么样的情况下会不能修完所有课:有环路!
1. BFS的思想,首先对于不需要prerequisite的课,可以直接take,不需要其他前提条件,当这些课take后,对于那些把这些课作为prerequisite的课来说,需要上的prerequisite就少了一门,如果少一门后需要上的预修课变为0,则说明该课也可以上了,相当于把课分层,先上最低等级的一些课,同时也是一步步排除高等级的课需要的预修课,从而可以上高等级的课。
再来分析需要什么数据结构来实现这个方法。首先,为了方便在发现一个新的可以上的课时找到把它作为预修课的所有课,从而对那些课的prerequisite数减1,我们需要一个list列出每门课是哪些课的预修课,因为课程编号是从0开始到n-1,所以可以考虑用数组存,每个数组里放这门课的高级课的list,考虑使用ArrayList[],需要注意的是,开一个ArrayList[n]数组只是开辟了这样一个存地址的空间,而对于数组中每个位置是没有初始化一个ArrayList的,所以还需要一个循环给每门课开一个list放入数组中。另外,需要一个数组记录bfs到当前阶段,每门课还需要的预修课的数量。如果该数为0或是变为0,说明该课可以上了,就将该课入队,每次从队首出队,对把这门课作为必修课的课需要的预修课数量减1。最后比较的是按这种方法是否所有的课都能修到。
(图,每次去掉一个入度(incoming degree)为0的点,并更新其他点的入度)
拓扑排序
即找出该图的一个线性序列,使得需要事先完成的事件总排在之后才能完成的事件之前。如果能找到这样一个线性序列,则该图是一个有向无环图。
注意从list中get的数是object不是int,需要转int。
class Solution { public boolean canFinish(int numCourses, int[][] prerequisites) { if(numCourses==0||prerequisites.length==0) return true; ArrayList[] pre=new ArrayList[numCourses]; for(int i=0;i<numCourses;i++){ pre[i]=new ArrayList();//注意需要初始化 } int[] count=new int[numCourses]; for(int[] p:prerequisites){ pre[p[1]].add(p[0]); count[p[0]]++; } Queue<Integer> queue=new LinkedList<>(); int c=0; for(int i=0;i<numCourses;i++){ if(count[i]==0){ queue.offer(i); c++; } } while(!queue.isEmpty()){ int a=queue.poll(); for(int i=0;i<pre[a].size();i++){ if(--count[(int)pre[a].get(i)]==0){//注意需要转int,直接从list得到的是object不是int c++; queue.offer((int)pre[a].get(i)); } } } return c==numCourses; } }
2. DFS
还是需要建立一个list,对于dfs,最直观的想法就是对于图找是否有环路。这种思想下,虽然修课是有先后顺序的,这是一个有向图,但因为只是要dfs找是否有环路,所以所有方向集体反向搜索效果是一样的,所以这里的list是针对每门课存这门课的所有prerequisites,还是针对每门课存把这门课作为prerequisites的课,效果是一样的。
(1)对于每门课,遍历它的预修课,找这门课向下深度搜索会不会有环路,如果这条路径中出现环路,返回false。
(2)对于每门课,遍历它的高级课,找这门课向上深度搜索会不会有环路,即某个路径上的低级课却是某高级课的更高级课。
以上两种是一样的。
class Solution { public boolean canFinish(int numCourses, int[][] prerequisites) { if(numCourses==0||prerequisites.length==0) return true; ArrayList[] pre=new ArrayList[numCourses]; for(int i=0;i<numCourses;i++){ pre[i]=new ArrayList(); } for(int[] p:prerequisites){ pre[p[1]].add(p[0]); } boolean[] visited=new boolean[numCourses];//记录当前搜索路径中是否已经访问过该点 boolean[] memo=new boolean[numCourses];//记录该点是否已经dfs遍历并返回true过 for(int i=0;i<numCourses;i++){ if(!dfs(pre, visited, memo,i)) return false; } return true; } public boolean dfs(ArrayList[] pre, boolean[] visited, boolean[] memo, int i){ if(memo[i]) return true; if(visited[i]) return false; visited[i]=true; for(int j=0;j<pre[i].size();j++){ if(!dfs(pre,visited,memo,(int)pre[i].get(j))) return false; memo[(int)pre[i].get(j)]=true;//上面刚刚提醒自己注意,写的时候还是忘了转int的事,要小心! } visited[i]=false; return true;//void的子函数写多了有时候就会忘记写最后一个return,要仔细啊! } }
注意两个boolean[]的使用,如果不用memo,则会有很多重复的dfs检索操作,用memo记录已经检查过的点,就可以避免重复对相同的点进行dfs。