🚩传送门:力扣题目

题目

你这个学期必须选修 [LC]207. 课程表 I - 图1 门课程,记为 [LC]207. 课程表 I - 图2[LC]207. 课程表 I - 图3

在选修某些课程之前需要一些先修课程。 先修课程按数组 [LC]207. 课程表 I - 图4给出,其中 [LC]207. 课程表 I - 图5 ,表示如果要学习课程 [LC]207. 课程表 I - 图6 必须 先学习课程 [LC]207. 课程表 I - 图7。例如,先修课程对[LC]207. 课程表 I - 图8表示:想要学习课程 [LC]207. 课程表 I - 图9 ,你需要先完成课程 [LC]207. 课程表 I - 图10

请你判断是否可能完成所有课程的学习 ?如果可以,返回 true;否则,返回 false

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]] 输出:true 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]] 输出:false 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

解题思路:拓扑排序

拓扑排序可以深搜(DFS)也可以广搜(BFS)

本题是一道经典的「拓扑排序」问题,给定一个包含 [LC]207. 课程表 I - 图11 个节点的有向图 [LC]207. 课程表 I - 图12,我们给出它的节点编号的一种排列。
如果满足:对于图[LC]207. 课程表 I - 图13中的任意一条有向边[LC]207. 课程表 I - 图14[LC]207. 课程表 I - 图15 在排列中都出现在 [LC]207. 课程表 I - 图16 的前面。那么称该排列是图 [LC]207. 课程表 I - 图17的「拓扑排序」。

根据上述的 定义,我们可以得出两个结论:

  • 如果图[LC]207. 课程表 I - 图18中存在环(即图[LC]207. 课程表 I - 图19不是「有向无环图」),那么图[LC]207. 课程表 I - 图20不存在拓扑排序

    这是因为假设图中存在环 [LC]207. 课程表 I - 图21,那么 [LC]207. 课程表 I - 图22在排列中必须出现在 [LC]207. 课程表 I - 图23 的前面,但 [LC]207. 课程表 I - 图24 同时也必须出现在[LC]207. 课程表 I - 图25 的前面,因此无法满足这个要求

  • 如果图[LC]207. 课程表 I - 图26是有向无环图,那么它的拓扑排序可能不止一种。

    举一个最极端的例子,如果图[LC]207. 课程表 I - 图27值包含[LC]207. 课程表 I - 图28 个节点却没有任何边,那么任意一种编号的排列都可以作为拓扑排序。

广度搜索
[LC]207. 课程表 I - 图29

复杂度分析

时间复杂度:[LC]207. 课程表 I - 图30,遍历一个图需要访问所有节点和所有临边, [LC]207. 课程表 I - 图31[LC]207. 课程表 I - 图32 分别为节点数量和临边数量;

空间复杂度:[LC]207. 课程表 I - 图33,为建立邻接表所需额外空间,[LC]207. 课程表 I - 图34 长度为 [LC]207. 课程表 I - 图35 ,并存储 [LC]207. 课程表 I - 图36 条临边的数据。

我的代码

  1. class Solution {
  2. public boolean canFinish(int numCourses, int[][] prerequisites) {
  3. // 入度数组
  4. int[] indegrees = new int[numCourses];
  5. // 存储边信息
  6. List<List<Integer>> edges = new ArrayList<>();
  7. Queue<Integer> queue = new LinkedList<>();
  8. for(int i = 0; i < numCourses; i++)
  9. edges.add(new ArrayList<>());
  10. // 获得每门课程的入度和边。
  11. for(int[] cp : prerequisites) {
  12. indegrees[cp[0]]++; // 入度自增
  13. edges.get(cp[1]).add(cp[0]); // 添加由自身u出发到v的边
  14. }
  15. // 获得入度为0的所有课程入队列
  16. for(int i = 0; i < numCourses; i++)
  17. if(indegrees[i] == 0) queue.add(i);
  18. // BFS 搜索
  19. while(!queue.isEmpty()) {
  20. int u = queue.poll();
  21. numCourses--;
  22. for(int v : edges.get(u))
  23. if(--indegrees[v] == 0) queue.add(v);
  24. }
  25. return numCourses == 0;
  26. }
  27. }

深度优先搜索
[LC]207. 课程表 I - 图37

复杂度分析

时间复杂度:[LC]207. 课程表 I - 图38,遍历一个图需要访问所有节点和所有临边, [LC]207. 课程表 I - 图39[LC]207. 课程表 I - 图40 分别为节点数量和临边数量;

空间复杂度:[LC]207. 课程表 I - 图41,为建立邻接表所需额外空间,[LC]207. 课程表 I - 图42 长度为 [LC]207. 课程表 I - 图43 ,并存储 [LC]207. 课程表 I - 图44 条临边的数据。

我的代码

  1. class Solution {
  2. boolean valid = true;
  3. public boolean canFinish(int numCourses, int[][] prerequisites) {
  4. List<List<Integer>> edges=new ArrayList<List<Integer>>();
  5. int[] visited = new int[numCourses];
  6. for (int i = 0; i < numCourses; ++i) {
  7. edges.add(new ArrayList<Integer>());
  8. }
  9. // 添加边的信息
  10. for (int[] info : prerequisites) {
  11. edges.get(info[1]).add(info[0]);
  12. }
  13. // 遍历每一个结点做起始结点开始dfs
  14. for (int i = 0; i < numCourses && valid; ++i) {
  15. if (visited[i] == 0) {
  16. dfs(i);
  17. }
  18. }
  19. return valid;
  20. }
  21. public void dfs(int u) {
  22. // 正在访问
  23. visited[u] = 1;
  24. // 一条由点 u -> v 的边
  25. for (int v: edges.get(u)) {
  26. // 未访问
  27. if (visited[v] == 0) {
  28. dfs(v);
  29. if (!valid) {
  30. return;
  31. }
  32. } else if (visited[v] == 1) {
  33. // 遇到一条正在访问的边,说明有环,刚才遍历过它
  34. valid = false;
  35. return;
  36. }
  37. }
  38. // 完成访问
  39. visited[u] = 2;
  40. }
  41. }