Baozi Training Leetcode solution 207. Course Schedule
Problem Statement
There are a total of numCourses
courses you have to take, labeled from 0
to numCourses-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?
Example 1:
Input: numCourses = 2, prerequisites = [[1,0]] Output: true Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.
Example 2:
Input: numCourses = 2, prerequisites = [[1,0],[0,1]] Output: false Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.
Constraints:
- The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
- You may assume that there are no duplicate edges in the input prerequisites.
1 <= numCourses <= 10^5
Problem link
Video Tutorial
You can find the detailed video tutorial here
Thought Process
It is a classic dependency graph problem. We can
translate this problem to direct if there is a cycle in a directed graph
or not. A text book solution is Kahn's algorithm for topological sorting.
We can have a simple way to represent the graph or use a more proper
adjacency lists (a little bit overkill for this problem though)
Solutions
Use adjacency lists BFS
1 private class Node { 2 public int val; 3 public List<Node> neighbors; 4 5 public Node(int val) { 6 this.val = val; 7 this.neighbors = new ArrayList<>(); 8 } 9 } 10 11 private List<Node> buildGraph(int num, int[][] prerequisites) { 12 List<Node> graph = new ArrayList<>(); 13 14 for (int i = 0; i < num; i++) { 15 graph.add(new Node(i)); 16 } 17 18 for (int i = 0; i < prerequisites.length; i++) { 19 graph.get(prerequisites[i][0]).neighbors.add(graph.get(prerequisites[i][1])); 20 } 21 22 return graph; 23 } 24 25 // model to a Nodes, still BFS with topological order to detect if a graph is a DAG 26 // https://www.geeksforgeeks.org/detect-cycle-in-a-directed-graph-using-bfs/ 27 public boolean canFinishGraph(int numCourses, int[][] prerequisites) { 28 if (numCourses <= 1 || prerequisites.length == 0 || prerequisites[0].length == 0) { 29 return true; 30 } 31 List<Node> graph = this.buildGraph(numCourses, prerequisites); 32 33 Queue<Node> q = new LinkedList<>(); 34 35 for (Node n : graph) { 36 if (n.neighbors.size() == 0) { 37 q.add(n); 38 } 39 } 40 41 Set<Node> visited = new HashSet<>(); 42 while (!q.isEmpty()) { 43 Node cur = q.poll(); 44 45 visited.add(cur); 46 47 for (Node n : graph) { 48 if (n.neighbors.contains(cur)) { 49 n.neighbors.remove(cur); 50 // only enqueue the nodes while there is no more neighbors 51 if (n.neighbors.size() == 0) { 52 q.add(n); 53 } 54 } 55 } 56 57 } 58 59 return visited.size() == numCourses; 60 } 61 @shixiaoyu 62
Time Complexity: O(V), since each vertex is visited only once during BFS
Space Complexity: O(V+E) since we use adjacency lists to represent a directed graph
Use simple hashmap BFS
1 public boolean canFinishBfsTopoSort(int numCourses, int[][] prerequisites) { 2 if (numCourses <= 1 || prerequisites.length == 0 || prerequisites[0].length == 0) { 3 return true; 4 } 5 6 Map<Integer, Set<Integer>> graph = new HashMap<>(); 7 8 // could be extracted into a build graph function 9 for (int i = 0; i < numCourses; i++) { 10 graph.put(i, new HashSet<>()); 11 } 12 13 for (int i = 0; i < prerequisites.length; i++) { 14 graph.get(prerequisites[i][0]).add(prerequisites[i][1]); 15 } 16 17 int coursesRemaining = numCourses; 18 Queue<Integer> queue = new LinkedList<>(); 19 20 // initialize 21 for (Map.Entry<Integer, Set<Integer>> entry : graph.entrySet()) { 22 // this is the reverse as the graph topological order, but it is the actual problem solving order 23 // e.g., a->b, -> reads as depends on, meaning you have to finish b to do a, so it will print out b, a 24 if (entry.getValue().size() == 0) { 25 queue.offer(entry.getKey()); 26 coursesRemaining--; 27 } 28 } 29 30 // BFS 31 while (!queue.isEmpty()) { 32 int key = queue.poll(); 33 System.out.println("** key: " + key); 34 for (Map.Entry<Integer, Set<Integer>> entry : graph.entrySet()) { 35 Set<Integer> curDependencies = entry.getValue(); 36 37 if (curDependencies.contains(key)) { 38 curDependencies.remove((Integer)key); // need to cast or else it will be remove(int index) 39 if (curDependencies.size() == 0) { // immediately check to avoid another loop 40 queue.offer(entry.getKey()); 41 coursesRemaining--; 42 } 43 } 44 } 45 } 46 47 return coursesRemaining == 0; 48 }
Time Complexity: O(V), since each vertex is visited only once during BFS
Use recursion DFS
1 // This is a DFS solution, basically just like detecting if a graph has loops. 2 // Used a lookup table prune, with lookup, this is O(V + E), same as BFS since each node is visited once and only once 3 public boolean canFinish(int numCourses, int[][] prerequisites) { 4 if (numCourses <= 1 || prerequisites.length == 0 || prerequisites[0].length == 0) { 5 return true; 6 } 7 8 // this is adjacency list, used a set to dedup 9 Map<Integer, Set<Integer>> graph = new HashMap<>(); 10 11 for (int i = 0; i < numCourses; i++) { 12 graph.put(i, new HashSet<>()); 13 } 14 15 for (int i = 0; i < prerequisites.length; i++) { 16 graph.get(prerequisites[i][0]).add(prerequisites[i][1]); 17 } 18 19 Map<Integer, Boolean> lookup = new HashMap<>(); 20 21 boolean[] visited = new boolean[numCourses]; 22 for (int i = 0; i < numCourses; i++) { 23 if (!this.explore(i, graph, visited, lookup)) { 24 return false; 25 } 26 } 27 return true; 28 } 29 30 // This is the DFS way to solve it, This could also generate a topological order, think about post order way 31 // return false if there is a loop, true if no loop 32 private boolean explore(int vertex, Map<Integer, Set<Integer>> graph, boolean[] visited, Map<Integer, Boolean> lookup) { 33 // keeps track of if the node has been visited or not, 34 // with this lookup, it's pruning (memorization so that we don't need to re-calculate previously visited node) 35 /* 36 1 37 / \ 38 0 3 - 4 39 \ 2 / 40 e.g., when recursion 0 -> 1 -> 3 -> 4 41 in 2nd round, 0 -> 2 -> 3(could directly return) 42 */ 43 if (lookup.containsKey(vertex)) { 44 return lookup.get(vertex); 45 } 46 47 visited[vertex] = true; // keeps track of visited or not during recursion 48 49 Set<Integer> dependencies = graph.get(vertex); 50 for (Integer i : dependencies) { 51 if (visited[i] || !this.explore(i, graph, visited, lookup)) { 52 return false; 53 } 54 } 55 visited[vertex] = false; 56 lookup.put(vertex, true); 57 return true; 58 }
Time Complexity: O(V), since each vertex is visited only once during BFS
References