The problem
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.
- For example, the pair
[0, 1]indicates that to take course0you have to first take course1.
Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.
Examples
Input: numCourses = 2, prerequisites = [[1,0]]
Output: [0,1]
Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1].
Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0,2,1,3]
Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0.
So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3].
Input: numCourses = 1, prerequisites = []
Output: [0]
Constraints
- 1 <= numCourses <= 2000
- 0 <= prerequisites.length <= numCourses * (numCourses - 1)
- prerequisites[i].length == 2
- 0 <= ai, bi < numCourses
- ai != bi
- All the pairs [ai, bi] are distinct.
Explanation
From the description of the problem, we learn that we are given n courses that are labeled from 0 to n - 1. We also know that some courses have prerequisites, and we want to return the order of courses that we need to take in order to finish all the courses. If it’s not possible, we will return an empty array.
Let’s look at the first example
We are given two courses 0 and 1 and prerequisites = [[1, 0]].
- In order to take course
1, we have to take course0first. As a result, we have the array[0, 1]. You can see that we can represent ourprerequisitesas a graph; therefore, we will be creating our solution around it.
Now, let’s look at another example when we have two prerequisites - [[1, 0], [0, 1]]
- The first prerequisite tells us that in order to take course
1, we need to take course0. - The second prerequisite tells us that in order to take course
0, we need to take course1. You can see that we can’t take those courses because there is no valid order, and our prerequisites form acycle. So as a result, we will return anemptyarray.
Topological Sort Solution
func findOrder(_ numCourses: Int, _ prerequisites: [[Int]]) -> [Int] {
var prereq: [Int: [Int]] = [:]
for element in prerequisites {
let crs = element[0]
let pre = element[1]
prereq[crs, default: []].append(pre)
}
var output: [Int] = []
var visited: Set<Int> = []
var cycle: Set<Int> = []
func dfs(_ crs: Int) -> Bool {
if cycle.contains(crs) {
return false
}
if visited.contains(crs) {
return true
}
cycle.insert(crs)
for pre in prereq[crs, default: []) {
if dfs(pre) == false {
return false
}
}
cycle.remove(crs)
visited.insert(crs)
output.append(crs)
return true
}
for c in 0 ..< numCourses {
if dfs(c) == false {
return []
}
}
return output
}
Explanation
We are going to solve this problem by using the topological sort algorithm. Let’s look at the example:
- The first step that we are going to do is to build an adjacency list that will tell us what prerequisites we have for each node. Next, we are going to build the output using the depth-first search algorithm, visiting every single node:
- We are going to start our DFS from node
0and visit our prerequisite1that we have in our adjacency list. - Next, we are going to visit the prerequisite of
1, which is the single node3. - Next, we are going to visit the prerequisite of
3, which is node2that has no prerequisites. Since node2has no prerequisites, we are allowed to take course2, and now we can add it to our output. We are also going to mark node2as visited so we don’t end up visiting it twice. - Next, in our DFS, we are going to go back to node
3, and since it does not have more prerequisites, we are going to mark it as visited and add it to our output. - Next, we are going back to node
1, and you can see that it does not have more prerequisites; therefore, we are going to mark it as visited and add it to our output. - Next, we are going to node
0. Node0has prerequisite1, which we already visited, and it also has a prerequisite with node2that we can ignore because we have already visited it earlier in our DFS. After that, we can mark node0as visited and add it to our result. - Next, we are going to visit node
4, mark it as visited, and add it to our output since it only has one prerequisite0that we already visited. - Lastly, we are going to do the same for node
5, mark it as visited, and add it to our output.
In case we detect a cycle, we will return an empty array.
Let’s look at an example with a cycle
We are going to follow the same steps as we did above, except now we have node 2 that points to node 0 and forms a cycle:
- We are going to start at node
0, next we are going to go to1, then we are going to go to3, then to node2, and lastly, we are going to go to node0. This will tell us that we found a cycle, and we are going to stop our algorithm and return[].
To detect a cycle, we will be using a hash set where we will store the current path, and it will help us recognize the cycle.
Time/Space complexity
- Time complexity: O(V + E)
- Space complexity: O(V + E)
- Where
Vis the number of courses andEis the number of prerequisites