Start learning free

LESSON 482 OF 543 · GRAPHS

Course Schedule II in Rust: Kahn's Algorithm with a Min-Heap

Practice O((V + E) log V) Read the full explanation

There are numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] means that you must take course bi before course ai.

For example, the pair [0, 1] means that to take course 0 you have to take course 1 first.

Return an order in which you can take all the courses, as a Vec of course numbers. A course is ready when all of its prerequisites are taken. Whenever several courses are ready, take the one with the smallest number next: this rule makes the order unique. If it is impossible to take all the courses, return an empty Vec.

 

Example 1:

Input: numCourses = 4, prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]
Output: [0, 1, 2, 3]
Explanation: Draw each pair [a, b] as an arrow b → a, from the course to take first:
    0 → 1 → 3
    0 → 2 → 3
At the start only course 0 is ready. Once it is taken, courses 1 and 2 are both ready: 1 is smaller,
so it comes first, then 2. Course 3 is ready last, when both 1 and 2 are taken.
The order 0, 2, 1, 3 also takes every course after its prerequisites, but it takes 2 while the
smaller course 1 is ready, so it is not the answer.

Example 2:

Input: numCourses = 5, prerequisites = [[1, 4], [0, 4], [2, 3]]
Output: [3, 2, 4, 0, 1]
Explanation: Courses 3 and 4 need nothing. 3 is smaller, so it comes first, and taking it
makes course 2 ready. Now 2 and 4 are ready, and 2 is smaller. Taking 4 makes courses 0 and 1 ready,
and they come last, the smaller one first.

Example 3:

Input: numCourses = 3, prerequisites = [[1, 0], [2, 1], [1, 2]]
Output: []
Explanation: Course 1 needs course 2 first, and course 2 needs course 1 first: they wait for
each other (a cycle), so neither of them can ever be taken. Course 0 can be taken, but not all
the courses, so the answer is empty.

 

Constraints:

  • 1 <= numCourses <= 105
  • 0 <= prerequisites.length <= 105
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • All the pairs prerequisites[i] are distinct.

Note:

Your algorithm should run in O((V + E) log V) time, where V is numCourses and E is the number of pairs, and use O(V + E) extra space.

The tutorial uses Kahn's algorithm, as in Course Schedule, with one change: the courses that are ready wait in a min-heap instead of a queue, so the next course taken is always the smallest ready one. Each course enters and leaves the heap once, in O(log V) each. If fewer than numCourses courses get taken, the rest wait on a cycle, or on a course that does, and no order exists. Looking through all the courses for the smallest ready one before each pick would cost O(V2) instead.

STEP 1 OF 17
    Ctrl + Enter to check

    RUST · GRAPHS

    Course Schedule Ii in Rust, explained

    Problem overview

    There are num_courses courses, numbered from 0. Each pair [a, b] in prerequisites means that course b must be taken before course a. The function find_order returns an order in which all the courses can be taken, as a Vec<i32>. A course is ready when all its prerequisites have been taken. When several courses are ready, the one with the smallest number goes next, which makes the order unique. If no order can take every course, the function returns an empty vector.

    For num_courses = 4 and prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]] the result is [0, 1, 2, 3]. Only course 0 is ready at first. Once it is taken, courses 1 and 2 are both ready and 1 comes first. Course 3 needs both. For [[1, 0], [2, 1], [1, 2]] with 3 courses the result is empty: courses 1 and 2 wait for each other, a cycle, so neither can ever be taken.

    Approach: Kahn's algorithm with a min-heap

    The prerequisites form a directed graph: each pair [a, b] is an arrow from b to a, from the course to take first to the course that needs it. An order that respects all the arrows is a topological order, and Kahn's algorithm builds one.

    For every course, count its prerequisites that haven't been taken yet, called its in-degree. A course with count 0 is ready. Take a ready course, add it to the order, and for each course that waits for it, lower that course's count by one. A count that reaches 0 means a new course is ready.

    The usual version keeps the ready courses in a queue. Here the rule is to take the smallest ready course, so they wait in a min-heap instead. Each course enters and leaves the heap once, at O(log V) each. Scanning all the courses for the smallest ready one before every pick would cost O(V²).

    If the order ends up with fewer courses than num_courses, the missing ones are on a cycle or wait for a course that is. Their counts never reach 0, and no order exists.

    Here is the run for num_courses = 5 and prerequisites = [[1, 4], [0, 4], [2, 3]]. The initial graph is [[], [], [], [2], [1, 0]] and in_degree is [1, 1, 1, 0, 0]. Courses 3 and 4 start in the heap.

    took 3   ready now [2, 4]
    took 2   ready now [4]
    took 4   ready now [0, 1]
    took 0   ready now [1]
    took 1   ready now []
    

    The order is [3, 2, 4, 0, 1]. Course 3 comes before 4 because it is smaller. Taking 3 readies course 2, which is smaller than 4, so 2 comes next.

    Step by step

    1. Build the graph as a mutable Vec<Vec<usize>> with one empty vector per course. graph[b] will list the courses that wait for course b. Courses are stored as usize, so they can index the vectors directly.
    2. Create in_degree as a mutable vector of zeros, one per course. in_degree[a] will count the prerequisites of course a not taken yet.
    3. Loop over &prerequisites, borrowing the vector. Each pair becomes one arrow.
    4. Push prereq[0] as usize onto graph[prereq[1] as usize]: course prereq[1] comes first, and course prereq[0] waits for it. The pairs hold i32, so as usize makes them indexes.
    5. Add 1 to in_degree[prereq[0] as usize]. That course has one more prerequisite. Rust has no ++ operator.
    6. Create ready, a mutable BinaryHeap<Reverse<usize>>. Rust's heap gives back its largest value first, and wrapping each course in Reverse flips the comparison so the smallest comes out first.
    7. Loop over every course from 0 to num_courses.
    8. Push Reverse(course) when its in_degree is 0. The courses that need nothing are where an order can start.
    9. Create order, a mutable Vec<i32>, for the courses in the order they are taken.
    10. Loop while the heap is not empty. Each course enters the heap at most once, so this runs at most once per course.
    11. Pop the smallest ready course with ready.pop().unwrap().0. This is the key line: taking the smallest ready course is what makes the order unique.
    12. Push course as i32 onto order.
    13. Loop over the courses that wait for it, &next_course over &graph[course].
    14. Subtract 1 from in_degree[next_course]: one of its prerequisites has been taken.
    15. Push Reverse(next_course) when its count is 0. Its last prerequisite was just taken. A count reaches 0 only once, so each course enters the heap once.
    16. If order.len() < num_courses as usize, return Vec::new(). Some courses never became ready.
    17. Return order as the last expression.

    The solution in Rust

    use std::cmp::Reverse;
    use std::collections::BinaryHeap;
    
    impl Solution {
        fn find_order(num_courses: i32, prerequisites: Vec<Vec<i32>>) -> Vec<i32> {
            let mut graph: Vec<Vec<usize>> = vec![Vec::new(); num_courses as usize];
            let mut in_degree = vec![0; num_courses as usize];
            for prereq in &prerequisites {
                graph[prereq[1] as usize].push(prereq[0] as usize);
                in_degree[prereq[0] as usize] += 1;
            }
            let mut ready: BinaryHeap<Reverse<usize>> = BinaryHeap::new();
            for course in 0..num_courses as usize {
                if in_degree[course] == 0 {
                    ready.push(Reverse(course));
                }
            }
            let mut order: Vec<i32> = Vec::new();
            while !ready.is_empty() {
                let course = ready.pop().unwrap().0;
                order.push(course as i32);
                for &next_course in &graph[course] {
                    in_degree[next_course] -= 1;
                    if in_degree[next_course] == 0 {
                        ready.push(Reverse(next_course));
                    }
                }
            }
            if order.len() < num_courses as usize {
                return Vec::new();
            }
            order
        }
    }
    

    The Rust details worth a closer look:

    • Reverse and the heap. BinaryHeap is a max-heap. std::cmp::Reverse is a one-field wrapper whose ordering is the opposite of the value inside it, so BinaryHeap<Reverse<usize>> pops the smallest course first. The popped value is a Reverse, so .0 takes the course out of it. The two use lines at the top of the lesson's file bring both names into scope.
    • vec![Vec::new(); n]. The macro clones the value for every slot, so each course gets its own empty vector. That requires the element to implement Clone, which Vec<usize> does.
    • Inferred types. The type of in_degree is not written. The literal 0 and the comparisons make it Vec<i32>. The type of graph is spelled out because Vec::new() alone doesn't say what the inner vectors hold.
    • Borrowing in the inner loop. for &next_course in &graph[course] borrows graph immutably while the body changes in_degree and ready. Those are separate variables, so the borrow checker accepts it.
    • pop().unwrap(). pop returns an Option. The loop condition checks the heap is not empty, so the unwrap can't fail.
    • Returning an empty vector. Vec::new() allocates nothing, and the last expression order has no semicolon, so it is the returned value.

    Complexity

    Time: O((V + E) log V), where V is the number of courses and E the number of pairs. Building the graph looks at each pair once. Each course is pushed to and popped from the heap at most once, at O(log V) each, and the inner loop looks at each pair once over the whole run.

    Space: O(V + E). The graph holds one entry per pair, and in_degree, ready and order each hold at most one entry per course.

    Edge cases and common mistakes

    • No prerequisites. With 3 courses and an empty list, every course starts ready and the result is [0, 1, 2]. With one course the result is [0].
    • A cycle. For the third example, only course 0 is taken, order has length 1 instead of 3, and the function returns an empty vector. A cycle in part of the graph makes the whole answer empty, even though some courses could be taken.
    • Using a plain queue. A queue also gives a valid topological order, but not the smallest-first one. For the first example both give [0, 1, 2, 3], but with [[1, 4], [0, 4], [2, 3]] a queue that starts from [3, 4] would take 3, then 4, then 2, which is not what the rule asks for.
    • Forgetting Reverse. Without it, the heap would pop the largest ready course first, and the order would break the smallest-first rule.
    • Swapping the pair. The pair [a, b] means b comes first. Reversing the roles gives a different order: with 2 courses and [[0, 1]] the result is [1, 0], not [0, 1].
    • Comparing the length with the wrong type. order.len() is a usize, so num_courses is converted with as usize for the comparison.

    Testing

    • 4 courses, [[1, 0], [2, 0], [3, 1], [3, 2]] gives [0, 1, 2, 3].
    • 5 courses, [[1, 4], [0, 4], [2, 3]] gives [3, 2, 4, 0, 1].
    • 3 courses, [[1, 0], [2, 1], [1, 2]] gives [].
    • 3 courses, no prerequisites, gives [0, 1, 2].
    • 1 course, no prerequisites, gives [0].
    • 2 courses, [[0, 1]] gives [1, 0].
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free