207.课程表
这时候你有可能会问“不是1203项目管理吗?”你怎么偷梁换柱?
实际上emmm,不妨让我们先熟悉熟悉拓扑排序。
思路一:拓扑排序
建议看官方解答的动图
此时,根据拓扑排序的结果,0是[1,4,7]的先决条件,但是[1,4,7]没有顺序关系。
应用场景:
如课程表的安排
重要结论:
- 只有有向无环图才有拓扑排序(否则会有相互依赖)
- 拓扑排序的结果不唯一。
具体实践1:BFS
具体思想是:
- while queue!=Empty:
- 用入度来描述每一门课程,入度为0表示这门课其先修课程已经修完(包含最最基础课程无先修课程的情况)
- 如果度数为0:
- 将其出列,加入的答案res中
- 每次将BFS队列中的一个元素出队列之后,修改其相关课程的入度情况。
- 比较输出的res长度和所有课程数量,如果相等,则是正常的,如果res数量小,说明存在环,则不存在拓扑排序。
- 一个技巧就是用LinkedList的方式实现图,用int[]的方式实现每个节点的编号
- 这样每个节点删除的时候就可以用遍历其LinkedList元素个数的方式把与其相连的点的入度-1。
class Solution {
List<List<Integer>> edges;
int[] indeg; // 用来统计每个点的入度
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 第一步:创建每个节点(课程)对应的edge,edge[i]是一个存储它后续课程的ArrayList
// 这样子,每当我们需要跟新的时候,我们只要对edge[i]中的每个元素的indeg--即可。
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// 第二步:统计每个节点的indegree。
indeg = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]]; //因为info[0]有一个先修课程info[1]
}
// 第三步:BFS的初始化,将所有没有先修课程的添加到queue中。
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}
// 第四步:将所有元素一次出队,并更新他们的indegree。
// visited表示我们目前可以安排的课程数量
int visited = 0;
while (!queue.isEmpty()) {
++visited;
int u = queue.poll();
for (int v: edges.get(u)) {
--indeg[v];
if (indeg[v] == 0) {
queue.offer(v);
}
}
}
// 当我们目前可以安排的课程数量等于总课程数量,说明没有环,返回True。
return visited == numCourses;
}
}
具体实践2:DFS
我们首先遍历最高级的课程,它的特点是出度为0。(出度的意思是当前元素被后面多少个元素需要)
我们利用DFS,每次将一个分支的课程从高级到低级一起添加到一个栈里面,然后倒序输出就是我们要的结果了。
对于图中的任意一个节点,它只可能有三种状态,即
- 未搜索:我们还没有搜索到这个节点
- 搜索中:它还没有出DFS搜索的栈Pile(出去的条件是当前节点的出度为0,说明它是最高级的课程了)如果在搜索过程中发现下一个元素也在搜索中,则出现了一个环(互相依赖),我们就返回False,即不存在拓扑排序结果。
- 已完成:我们添加到用于保存结果的栈中。
官方解答和上述描述的算法不太一样,而是根据题目特点,直接检查是否有环,如果有环就返回false。
class Solution {
List<List<Integer>> edges;
int[] visited; // 用来记录每个元素的状态,0是为搜索,1是搜索中,2是搜索完成
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// 初始化所有的边,这里边[i,j]表示i是j的后续课程。
visited = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 如果检测到一个环就直接跳出去
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
return valid;
}
public void dfs(int u) {
// 因为dfs的条件是当前元素没有出度,所以它一定是
visited[u] = 1;
// 检测v的上层元素
for (int v: edges.get(u)) {
// 如果是visited[v]==2,即已经完成搜索了
if (visited[v] == 0) {
dfs(v); //如果上一层
if (!valid) {
return;
}
}
// 如果检测到一个环
else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}
}
收获:
- 对于
int [][] a
可以用for(int [] tmp : a)
的方式调用其内部元素。 - 对于
List<List<Integer>> tmp
,我们初始化的时候:tmp = new ArrayList<List<Integer>>()
然后,tmp.add(new ArrayList<Integer>())
。即List的具体实现为ArrayList。
自己尝试复现代码
BFS
class Solution {
List<List<Integer>> edges;
int[] indeg;
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 第一步:初始化+读取每一条边
edges = new ArrayList<List<Integer>>();
for(int i=0;i<numCourses;i++)
edges.add(new ArrayList<Integer>());
// 第二步:初始化edges+indeg
indeg = new int[numCourses];
for(int[] courses : prerequisites){
// 添加某一个课程classes[1]的后续课程
edges.get(courses[1]).add(courses[0]);
indeg[courses[0]]++;
}
// 第三步:初始化bfs
Queue<Integer> q = new LinkedList<>();
for(int i=0;i<numCourses;i++)
if(indeg[i]==0)
q.offer(i);
// 第四步:bfs
int visited = 0; //用来记录目前我们已经安排的课程
while(!q.isEmpty()){
int crtcourse = q.poll();
for(int latterCourse: edges.get(crtcourse)){
indeg[latterCourse]--;
// 如果当前课程为0,则我们可以添加到队列中了(可以被安排)
if(indeg[latterCourse]==0)
q.offer(latterCourse);
}
visited++;
}
return visited==numCourses;
}
}