题目描述(中等难度)

210. Course Schedule II - 图1

207 题 Course Schedule 的延伸,给定 n 组先修课的关系,[m,n] 代表在上 m 这门课之前必须先上 n 这门课。输出一个上课序列。

思路分析

207 题 考虑是否存在一个序列上完所有课,这里的话,换汤不换药,完全可以按照 207 题 的解法改出来,大家可以先去看一下。主要是两种思路,BFSDFS ,题目就是在考拓扑排序。

解法一

先把 207 题 的思路贴过来。

把所有的关系可以看做图的边,所有的边构成了一个有向图。

对于[[1,3],[1,4],[2,4],[3,5],[3,6],[4,6]] 就可以看做下边的图,箭头指向的是需要先上的课。

210. Course Schedule II - 图2

想法很简单,要想上完所有的课,一定会有一些课没有先修课,比如上图的 56。然后我们可以把 56 节点删去。

210. Course Schedule II - 图3

然后 34 就可以上了,同样的道理再把 34 删去。

接下来就可以去学 12 了。因此可以完成所有的课。

代码的话,用邻接表表示图。此外,我们不需要真的去删除节点,我们可以用 outNum 变量记录所有节点的先修课门数。当删除一个节点的时候,就将相应节点的先修课个数减一即可。

最后只需要判断所有的节点的先修课门数是否全部是 0 即可。

在这道题的话,改之前的代码也很简单,只需要把每次出队的元素保存起来即可。

  1. public int[] findOrder(int numCourses, int[][] prerequisites) {
  2. // 保存每个节点的先修课个数,也就是出度
  3. HashMap<Integer, Integer> outNum = new HashMap<>();
  4. // 保存以 key 为先修课的列表,也就是入度的节点
  5. HashMap<Integer, ArrayList<Integer>> inNodes = new HashMap<>();
  6. // 保存所有节点
  7. HashSet<Integer> set = new HashSet<>();
  8. int rows = prerequisites.length;
  9. for (int i = 0; i < rows; i++) {
  10. int key = prerequisites[i][0];
  11. int value = prerequisites[i][1];
  12. set.add(key);
  13. set.add(value);
  14. if (!outNum.containsKey(key)) {
  15. outNum.put(key, 0);
  16. }
  17. if (!outNum.containsKey(value)) {
  18. outNum.put(value, 0);
  19. }
  20. // 当前节点先修课个数加一
  21. int num = outNum.get(key);
  22. outNum.put(key, num + 1);
  23. if (!inNodes.containsKey(value)) {
  24. inNodes.put(value, new ArrayList<>());
  25. }
  26. // 更新以 value 为先修课的列表
  27. ArrayList<Integer> list = inNodes.get(value);
  28. list.add(key);
  29. }
  30. // 将当前先修课个数为 0 的课加入到队列中
  31. Queue<Integer> queue = new LinkedList<>();
  32. for (int k : set) {
  33. if (outNum.get(k) == 0) {
  34. queue.offer(k);
  35. }
  36. }
  37. int[] res = new int[numCourses];
  38. int count = 0;
  39. while (!queue.isEmpty()) {
  40. // 队列拿出来的课代表要删除的节点
  41. // 要删除的节点的 list 中所有课的先修课个数减一
  42. int v = queue.poll();
  43. //**************主要修改的地方********************//
  44. res[count++] = v;
  45. //**********************************************//
  46. ArrayList<Integer> list = inNodes.getOrDefault(v, new ArrayList<>());
  47. for (int k : list) {
  48. int num = outNum.get(k);
  49. // 当前课的先修课要变成 0, 加入队列
  50. if (num == 1) {
  51. queue.offer(k);
  52. }
  53. // 当前课的先修课个数减一
  54. outNum.put(k, num - 1);
  55. }
  56. }
  57. for (int k : set) {
  58. if (outNum.get(k) != 0) {
  59. //有课没有完成,返回空数组
  60. return new int[0];
  61. }
  62. }
  63. //**************主要修改的地方********************//
  64. HashSet<Integer> resSet = new HashSet<>();
  65. for (int i = 0; i < count; i++) {
  66. resSet.add(res[i]);
  67. }
  68. //有些课是独立存在的,这些课可以随时上,添加进来
  69. for (int i = 0; i < numCourses; i++) {
  70. if (!resSet.contains(i)) {
  71. res[count++] = i;
  72. }
  73. }
  74. //**********************************************//
  75. return res;
  76. }

上边的代码就是要注意一些课,既没有先修课,也不是别的课的先修课,所以这些课什么时候上都可以,在最后加进来即可。

解法二

同样的,先把 207 题 的思路贴过来。

还有另一种思路,我们只需要一门课一门课的判断。

从某门课开始遍历,我们通过 DFS 一条路径一条路径的判断,保证过程中没有遇到环。

210. Course Schedule II - 图4

深度优先遍历 1,相当于 3 条路径

1 -> 3 -> 51 -> 3 -> 61 -> 4 -> 6

深度优先遍历 2,相当于 1 条路径

2 -> 4 -> 6

深度优先遍历 3,相当于 2 条路径

3 -> 53 -> 6

深度优先遍历 4,相当于 1 条路径

4 -> 6

深度优先遍历 5,相当于 1 条路径

5

深度优先遍历 6,相当于 1 条路径

6

什么情况下不能完成所有课程呢?某条路径出现了环,如下图。

210. Course Schedule II - 图5

出现了 1 -> 3 -> 6 -> 3。所以不能学完所有课程。

代码的话,用邻接表表示图。通过递归实现 DFS ,用 visited 存储当前路径上的节点。

同时用 visitedFinish 表示可以学完的课程,起到优化算法的作用。

在这道题的话,我们只需要在 dfs 中把叶子节点加入,并且如果当前节点的所有先修课已经完成,也将其加入。在代码中就体现在完成了 for 循环的时候。

  1. public int[] findOrder(int numCourses, int[][] prerequisites) {
  2. HashMap<Integer, ArrayList<Integer>> outNodes = new HashMap<>();
  3. HashSet<Integer> set = new HashSet<>();
  4. int rows = prerequisites.length;
  5. for (int i = 0; i < rows; i++) {
  6. int key = prerequisites[i][0];
  7. int value = prerequisites[i][1];
  8. set.add(key);
  9. if (!outNodes.containsKey(key)) {
  10. outNodes.put(key, new ArrayList<>());
  11. }
  12. // 存储当前节点的所有先修课程
  13. ArrayList<Integer> list = outNodes.get(key);
  14. list.add(value);
  15. }
  16. int[] res = new int[numCourses];
  17. HashSet<Integer> resSet = new HashSet<>(); //防止重复的节点加入
  18. HashSet<Integer> visitedFinish = new HashSet<>();
  19. // 判断每一门课
  20. for (int k : set) {
  21. if (!dfs(k, outNodes, new HashSet<>(), visitedFinish, res, resSet)) {
  22. return new int[0];
  23. }
  24. visitedFinish.add(k);
  25. }
  26. //和之前一样,把独立的课加入
  27. for (int i = 0; i < numCourses; i++) {
  28. if (!resSet.contains(i)) {
  29. res[count++] = i;
  30. }
  31. }
  32. return res;
  33. }
  34. int count = 0;
  35. private boolean dfs(int start, HashMap<Integer, ArrayList<Integer>> outNodes, HashSet<Integer> visited,
  36. HashSet<Integer> visitedFinish, int[] res, HashSet<Integer> resSet) {
  37. // 已经处理过
  38. if (visitedFinish.contains(start)) {
  39. return true;
  40. }
  41. //**************主要修改的地方********************//
  42. // 到了叶子节点
  43. if (!outNodes.containsKey(start)) {
  44. if (!resSet.contains(start)) {
  45. resSet.add(start);
  46. res[count++] = start;
  47. }
  48. return true;
  49. }
  50. //**********************************************//
  51. // 出现了环
  52. if (visited.contains(start)) {
  53. return false;
  54. }
  55. // 将当前节点加入路径
  56. visited.add(start);
  57. ArrayList<Integer> list = outNodes.get(start);
  58. for (int k : list) {
  59. if (!dfs(k, outNodes, visited, visitedFinish, res, resSet)) {
  60. return false;
  61. }
  62. }
  63. //**************主要修改的地方********************//
  64. if (!resSet.contains(start)) {
  65. resSet.add(start);
  66. res[count++] = start;
  67. }
  68. //**********************************************//
  69. visited.remove(start);
  70. return true;
  71. }

我们分别用数组 res 和集合 resSet 存储最终的结果,因为 DFS 中可能经过重复的节点,resSet 可以保证我们不添加重复的节点。

总体上和 207 题 是一样的,一些细节的地方注意到了即可。当然上边的代码因为是在 207 题的基础上改的,所以可能不够简洁,仅供参考,总体思想就是 BFSDFS

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情