Course Schedule - Task Ordering with Prerequisites
Problem and examples
You are given n tasks labeled from 0 to n - 1 and an array of prerequisite pairs. Each pair [a, b] indicates that task b must be completed before task a.
Return any valid order in which the tasks can be completed. If no such order exists (due to a cycle), return an empty array.
| n | Prerequisites | Valid Output | Description |
|---|---|---|---|
| 2 | [[1, 0]] | [0, 1] | Task 1 depends on 0, so 0 must come first |
| 4 | [[1, 0], [2, 0], [3, 1], [3, 2]] | [0, 1, 2, 3] | Task 3 depends on both 1 and 2, which in turn depend on 0 |
| 1 | [] | [0] | Single task with no prerequisites |
| 3 | [[0, 1], [1, 2], [2, 0]] | [] | Cycle detected, impossible to complete all tasks |
| 5 | [] | [0, 1, 2, 3, 4] | No dependencies, any order is valid |
How it works
Understanding the Problem
We are given a number of courses, and a list of prerequisite pairs. Each pair [a, b] means you must complete course b before course a. Our task is to determine if it’s possible to finish all courses, and if yes, return one possible order in which to take them.
This is a classic problem of dependency resolution and can be modeled as a directed graph where each course is a node, and each prerequisite is a directed edge. If the graph contains a cycle, then it is impossible to finish all courses. Otherwise, we can generate a valid course order using topological sorting.
Step-by-Step Solution with Example
Step 1: Represent the Problem as a Graph
We treat each course as a node. If course b is a prerequisite for course a, we draw a directed edge from b to a.
Example:
Let’s say numCourses = 4 and prerequisites = [[1,0],[2,0],[3,1],[3,2]].
This means:
- Take course 0 before 1
- Take course 0 before 2
- Take course 1 before 3
- Take course 2 before 3
So, the graph looks like:
0 → 1 → 3
↑
→ 2 ———┘
Step 2: Initialize In-Degree Array and Adjacency List
The in-degree of a node is the number of prerequisites it has. We create:
- An adjacency list to store which courses depend on each course.
- An in-degree array to count prerequisites for each course.
Step 3: Enqueue Courses with In-Degree 0
Courses with no prerequisites (in-degree 0) can be taken immediately. We add them to a queue.
In our example, only course 0 has in-degree 0 initially, so we enqueue it.
Step 4: Process the Queue
While the queue is not empty:
- Remove a course from the queue and add it to the result list.
- For each course that depends on it, reduce its in-degree by 1.
- If any course’s in-degree becomes 0, enqueue it.
For our example:
- Start with [0]
- Process 0 → add [1, 2] to queue → result: [0]
- Process 1 → reduce in-degree of 3 → result: [0, 1]
- Process 2 → reduce in-degree of 3 → result: [0, 1, 2]
- Now 3 has in-degree 0 → enqueue → result: [0, 1, 2, 3]
Step 5: Check if All Courses Are Taken
If the result list contains all courses, we return it. If not, it means there’s a cycle and we return an empty array.
Edge Cases
- Cycle in prerequisites: Example:
[[1,0],[0,1]]. This creates a loop: 0 → 1 → 0. It’s impossible to finish all courses. - No prerequisites: All courses are independent. Any order is valid.
- Single course: Always possible to finish it.
- Multiple valid orders: For acyclic graphs, many topological sorts are possible.
Finally
This problem teaches us how to model real-world scheduling problems using graphs. Topological sorting is the key idea, and cycle detection helps us rule out impossible scenarios. By translating the prerequisites into a graph and using in-degree tracking, we can determine if all tasks (courses) can be completed, and in what order.
- Create an adjacency list for the graph.
- Create an array to track in-degree for each task.
- Populate the adjacency list and in-degree array based on prerequisites.
- Initialize a queue with all tasks having in-degree 0.
- While the queue is not empty:
- Remove a task from the queue and add it to the result list.
- For each neighbor of that task, reduce its in-degree by 1.
- If any neighbor’s in-degree becomes 0, add it to the queue.
- After processing, if the result contains all tasks, return it.
- Otherwise, return an empty array (cycle detected).
Implementation and efficiency
C implementation
#include <stdio.h>
#include <stdlib.h>
#define MAX 1000
int adj[MAX][MAX], inDegree[MAX], queue[MAX];
int front = 0, rear = -1;
void findOrder(int n, int prerequisites[][2], int m) {
for (int i = 0; i < m; i++) {
int a = prerequisites[i][0];
int b = prerequisites[i][1];
adj[b][a] = 1;
inDegree[a]++;
}
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) queue[++rear] = i;
}
int result[MAX], idx = 0;
while (front <= rear) {
int node = queue[front++];
result[idx++] = node;
for (int i = 0; i < n; i++) {
if (adj[node][i]) {
inDegree[i]--;
if (inDegree[i] == 0) queue[++rear] = i;
}
}
}
if (idx != n) printf("[]\n");
else {
printf("[ ");
for (int i = 0; i < idx; i++) printf("%d ", result[i]);
printf("]\n");
}
}
int main() {
int prerequisites1[][2] = {{1, 0}};
findOrder(2, prerequisites1, 1);
int prerequisites2[][2] = {{1, 0}, {2, 0}, {3, 1}, {3, 2}};
findOrder(4, prerequisites2, 4);
return 0;
}C++ implementation
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<int> findOrder(int n, vector<pair<int, int>>& prerequisites) {
vector<vector<int>> adj(n);
vector<int> inDegree(n, 0);
for (auto& p : prerequisites) {
adj[p.second].push_back(p.first);
inDegree[p.first]++;
}
queue<int> q;
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) q.push(i);
}
vector<int> result;
while (!q.empty()) {
int node = q.front(); q.pop();
result.push_back(node);
for (int neighbor : adj[node]) {
if (--inDegree[neighbor] == 0) q.push(neighbor);
}
}
return result.size() == n ? result : vector<int>();
}
int main() {
vector<pair<int, int>> prerequisites1 = {{1, 0}};
auto result1 = findOrder(2, prerequisites1);
for (int x : result1) cout << x << " "; cout << endl;
vector<pair<int, int>> prerequisites2 = {{1, 0}, {2, 0}, {3, 1}, {3, 2}};
auto result2 = findOrder(4, prerequisites2);
for (int x : result2) cout << x << " "; cout << endl;
return 0;
}Python implementation
from collections import deque, defaultdict
def find_order(n, prerequisites):
adj = defaultdict(list)
in_degree = [0] * n
for a, b in prerequisites:
adj[b].append(a)
in_degree[a] += 1
queue = deque([i for i in range(n) if in_degree[i] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in adj[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result if len(result) == n else []
print("Order for 2 tasks:", find_order(2, [[1, 0]]))
print("Order for 4 tasks:", find_order(4, [[1, 0], [2, 0], [3, 1], [3, 2]]))Java implementation
import java.util.*;
public class TaskOrder {
public static List<Integer> findOrder(int n, int[][] prerequisites) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[] inDegree = new int[n];
for (int[] p : prerequisites) {
adj.get(p[1]).add(p[0]);
inDegree[p[0]]++;
}
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) q.add(i);
}
List<Integer> result = new ArrayList<>();
while (!q.isEmpty()) {
int node = q.poll();
result.add(node);
for (int neighbor : adj.get(node)) {
if (--inDegree[neighbor] == 0) q.add(neighbor);
}
}
return result.size() == n ? result : new ArrayList<>();
}
public static void main(String[] args) {
int[][] prerequisites1 = {{1, 0}};
System.out.println("Order for 2 tasks: " + findOrder(2, prerequisites1));
int[][] prerequisites2 = {{1, 0}, {2, 0}, {3, 1}, {3, 2}};
System.out.println("Order for 4 tasks: " + findOrder(4, prerequisites2));
}
}JavaScript implementation
function findOrder(n, prerequisites) {
const adj = Array.from({ length: n }, () => []);
const inDegree = new Array(n).fill(0);
for (const [a, b] of prerequisites) {
adj[b].push(a);
inDegree[a]++;
}
const queue = [], result = [];
for (let i = 0; i < n; i++) {
if (inDegree[i] === 0) queue.push(i);
}
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
for (const neighbor of adj[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return result.length === n ? result : [];
}
console.log("Order for 2 tasks:", findOrder(2, [[1, 0]]));
console.log("Order for 4 tasks:", findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]]));Go implementation
package main
import (
"fmt"
)
func findOrder(n int, prerequisites [][]int) []int {
adj := make([][]int, n)
inDegree := make([]int, n)
for _, p := range prerequisites {
a, b := p[0], p[1]
adj[b] = append(adj[b], a)
inDegree[a]++
}
queue := []int{}
for i := 0; i < n; i++ {
if inDegree[i] == 0 {
queue = append(queue, i)
}
}
result := []int{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
for _, neighbor := range adj[node] {
inDegree[neighbor]--
if inDegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
if len(result) != n {
return []int{}
}
return result
}
func main() {
fmt.Println("Order for 2 tasks:", findOrder(2, [][]int{{1, 0}}))
fmt.Println("Order for 4 tasks:", findOrder(4, [][]int{{1, 0}, {2, 0}, {3, 1}, {3, 2}}))
}Rust implementation
use std::collections::VecDeque;
fn find_order(n: usize, prerequisites: Vec<(usize, usize)>) -> Vec<usize> {
let mut adj = vec![vec![]; n];
let mut in_degree = vec![0; n];
for (a, b) in prerequisites {
adj[b].push(a);
in_degree[a] += 1;
}
let mut queue: VecDeque<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
let mut result = vec![];
while let Some(node) = queue.pop_front() {
result.push(node);
for &neighbor in &adj[node] {
in_degree[neighbor] -= 1;
if in_degree[neighbor] == 0 {
queue.push_back(neighbor);
}
}
}
if result.len() == n { result } else { vec![] }
}
fn main() {
println!("Order for 2 tasks: {:?}", find_order(2, vec![(1, 0)]));
println!("Order for 4 tasks: {:?}", find_order(4, vec![(1, 0), (2, 0), (3, 1), (3, 2)]));
}Kotlin implementation
fun findOrder(n: Int, prerequisites: Array<IntArray>): List<Int> {
val adj = Array(n) { mutableListOf<Int>() }
val inDegree = IntArray(n)
for ((a, b) in prerequisites) {
adj[b].add(a)
inDegree[a]++
}
val queue = ArrayDeque<Int>()
for (i in 0 until n) {
if (inDegree[i] == 0) queue.add(i)
}
val result = mutableListOf<Int>()
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
result.add(node)
for (neighbor in adj[node]) {
inDegree[neighbor]--
if (inDegree[neighbor] == 0) queue.add(neighbor)
}
}
return if (result.size == n) result else emptyList()
}
fun main() {
println("Order for 2 tasks: ${findOrder(2, arrayOf(intArrayOf(1, 0)))}")
println("Order for 4 tasks: ${findOrder(4, arrayOf(intArrayOf(1, 0), intArrayOf(2, 0), intArrayOf(3, 1), intArrayOf(3, 2)))}")
}Swift implementation
func findOrder(_ n: Int, _ prerequisites: [[Int]]) -> [Int] {
var adj = Array(repeating: [Int](), count: n)
var inDegree = Array(repeating: 0, count: n)
for p in prerequisites {
adj[p[1]].append(p[0])
inDegree[p[0]] += 1
}
var queue = [Int](), result = [Int]()
for i in 0..<n {
if inDegree[i] == 0 {
queue.append(i)
}
}
while !queue.isEmpty {
let node = queue.removeFirst()
result.append(node)
for neighbor in adj[node] {
inDegree[neighbor] -= 1
if inDegree[neighbor] == 0 {
queue.append(neighbor)
}
}
}
return result.count == n ? result : []
}
print("Order for 2 tasks:", findOrder(2, [[1, 0]]))
print("Order for 4 tasks:", findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]]))TypeScript implementation
function findOrder(n: number, prerequisites: number[][]): number[] {
const adj: number[][] = Array.from({ length: n }, () => []);
const inDegree: number[] = Array(n).fill(0);
for (const [a, b] of prerequisites) {
adj[b].push(a);
inDegree[a]++;
}
const queue: number[] = [];
const result: number[] = [];
for (let i = 0; i < n; i++) {
if (inDegree[i] === 0) queue.push(i);
}
while (queue.length > 0) {
const node = queue.shift()!;
result.push(node);
for (const neighbor of adj[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return result.length === n ? result : [];
}
console.log("Order for 2 tasks:", findOrder(2, [[1, 0]]));
console.log("Order for 4 tasks:", findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]]));Time complexity
| Case | Time | Explanation |
|---|---|---|
| best case | O(V + E) | In all cases, we must visit every node and edge to build the graph and perform topological sorting. |
| average case | O(V + E) | Each course and each prerequisite relation must be examined exactly once. |
| worst case | O(V + E) | Even with complex dependencies, we still process every node and edge once for sorting and cycle detection. |
Space complexity
O(V + E)
We use an adjacency list (O(E)) and an in-degree array and queue (O(V)) to store and process the graph.