Start learning free

LESSON 474 OF 535 · GRAPHS

Course Schedule II in C++: 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 a vector 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 vector 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 vector.

 

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

    C++ · GRAPHS

    Course Schedule Ii in C++, explained

    Problem overview

    There are numCourses courses labeled 0 to numCourses - 1. Each pair [a, b] in prerequisites means course b must be taken before course a. The function findOrder returns an order in which all the courses can be taken. A course is ready when all its prerequisites have been taken, and whenever several courses are ready, the smallest number goes next. That rule makes the answer unique. If taking all the courses is impossible, the function returns an empty vector.

    For numCourses = 4 and prerequisites [[1, 0], [2, 0], [3, 1], [3, 2]], only course 0 is ready at the start. After it, courses 1 and 2 are ready and 1 comes first, then 2, then 3. The answer is [0, 1, 2, 3]. For [[1, 0], [2, 1], [1, 2]] with three courses, courses 1 and 2 wait for each other, so the answer is [].

    Approach: take ready courses, smallest first

    Think of each pair [a, b] as an arrow from b to a, the course to take first pointing to the course that needs it. A valid order is a topological order of this graph: every course comes after all the courses that point to it. Kahn's algorithm builds one by repeatedly taking a course with no remaining prerequisites.

    It keeps an inDegree count for each course, the number of its prerequisites not yet taken. Courses with count 0 are ready. Taking a course removes its arrows: every course that was waiting for it gets its count reduced by 1, and if the count reaches 0, that course becomes ready too.

    The plain version keeps the ready courses in a queue. Here the statement asks for the smallest ready course every time, so the ready courses wait in a min-heap. On the second example, numCourses = 5 and [[1, 4], [0, 4], [2, 3]], a plain queue would give [3, 4, 2, 1, 0], which is a valid topological order but not the smallest-first one. With the heap, the result is [3, 2, 4, 0, 1]: after 3 is taken, 2 and 4 are both ready and 2 is smaller.

    If the loop ends with fewer than numCourses courses taken, the rest are on a cycle, or wait for a course that is. Their counts never reach 0, so they never enter the heap, and no order exists.

    The statement sets O((V + E) log V) time, with V courses and E pairs. Each course is pushed and popped once at O(log V) each, and each pair is looked at once. Searching all the courses for the smallest ready one before every pick would cost O(V * V).

    For the first example, the graph lists after the pairs are read are:

    graph[0] = [1, 2]
    graph[1] = [3]
    graph[2] = [3]
    graph[3] = []
    inDegree = [0, 1, 1, 2]

    Step by step

    1. Create graph as a vector<vector<int>> of numCourses empty vectors. Giving the constructor only a size makes that many empty inner vectors. graph[b] will list the courses waiting for course b.
    2. Create inDegree, a vector<int> of numCourses zeros. inDegree[a] counts the prerequisites of course a that are not taken yet.
    3. Loop over prerequisites with auto& prereq. The & reads each pair in place instead of copying it.
    4. Push prereq[0] onto graph[prereq[1]]. Course prereq[1] comes first, and course prereq[0] waits for it.
    5. Increment inDegree[prereq[0]]. That course has one more prerequisite.
    6. Create the min-heap ready, a priority_queue<int, vector<int>, greater<int>>. A priority_queue keeps its largest value on top, and greater<int> flips that, so top() is the smallest.
    7. Push every course whose inDegree is 0 onto ready. A valid order can only start with courses that need nothing.
    8. Create the empty vector order for the courses in the order they are taken.
    9. Loop while the heap is not empty. Each course enters the heap at most once, so the loop runs at most once per course.
    10. Read ready.top() into course and pop it. This is the key line: of all the ready courses, the smallest number is taken. pop returns nothing, so the top is read first.
    11. Append course to order.
    12. Loop over graph[course], the courses waiting for it. For each nextCourse, decrement inDegree[nextCourse]: one of its prerequisites is now taken.
    13. If the count reaches 0, push nextCourse onto ready. A count reaches 0 only once, so no course enters the heap twice.
    14. After the loop, if order is shorter than numCourses, return {}. Cast the size to int for the comparison.
    15. Otherwise return order.

    The solution in C++

    class Solution {
    public:
        vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
            vector<vector<int>> graph(numCourses);
            vector<int> inDegree(numCourses, 0);
            for (auto& prereq : prerequisites) {
                graph[prereq[1]].push_back(prereq[0]);
                inDegree[prereq[0]]++;
            }
            priority_queue<int, vector<int>, greater<int>> ready;
            for (int course = 0; course < numCourses; course++) {
                if (inDegree[course] == 0) {
                    ready.push(course);
                }
            }
            vector<int> order;
            while (!ready.empty()) {
                int course = ready.top();
                ready.pop();
                order.push_back(course);
                for (int nextCourse : graph[course]) {
                    inDegree[nextCourse]--;
                    if (inDegree[nextCourse] == 0) {
                        ready.push(nextCourse);
                    }
                }
            }
            if ((int) order.size() < numCourses) {
                return {};
            }
            return order;
        }
    };
    

    The C++ details worth a closer look:

    • The min-heap type. priority_queue<int, vector<int>, greater<int>> has three template arguments: the element type, the container that stores it, and the comparison. The default comparison, less<int>, makes a max-heap. The container argument has to be given to reach the third.
    • Heap costs. push and pop take O(log n) and top takes O(1). top() returns a reference to the smallest element, and int course = ready.top() copies it, which is why it is safe to call ready.pop() on the next line.
    • (int) order.size(). size() is unsigned. Comparing it directly with an int converts the int to unsigned, which is harmless here but triggers a compiler warning, and it would turn a negative number into a huge one. The cast keeps the comparison between two int values.
    • return {}; builds an empty vector<int> from the function's return type.
    • Range-based loops. for (int nextCourse : graph[course]) copies each int, which is fine for small values. auto& prereq avoids copying each inner vector.

    Complexity

    Let V be numCourses and E the number of prerequisite pairs.

    Time: O((V + E) log V). Building the graph takes O(V + E). Every course is pushed onto the heap at most once and popped once, each in O(log V), and every pair is looked at once when its first course is taken.

    Space: O(V + E). The graph holds one entry per pair, inDegree and order hold V numbers each, and the heap holds at most V courses.

    Edge cases and common mistakes

    • No prerequisites. With numCourses = 3 and no pairs, every course is ready at once and the answer is [0, 1, 2]. A single course gives [0].
    • A cycle of two. With two courses and [[0, 1], [1, 0]], no course starts with count 0, the heap stays empty, and the function returns [].
    • The smallest number is not always first. With numCourses = 3 and [[0, 2], [1, 2]], courses 0 and 1 wait for 2, so the answer is [2, 0, 1].
    • Reversing the arrow. In [a, b], b comes first. Storing the arrow the other way round would list courses before the ones they need.
    • Using a plain queue. The order is still valid but not the smallest-first one the statement asks for, as the second example shows.
    • A partial cycle. Courses outside a cycle may still be taken, but the function must return [] when any course cannot be taken, which is what the length check does. In the third example course 0 is taken, but the answer is still empty.

    Testing

    • numCourses = 4, [[1, 0], [2, 0], [3, 1], [3, 2]] gives [0, 1, 2, 3].
    • numCourses = 5, [[1, 4], [0, 4], [2, 3]] gives [3, 2, 4, 0, 1].
    • numCourses = 3, [[1, 0], [2, 1], [1, 2]] gives [].
    • numCourses = 1, no pairs, gives [0].
    • numCourses = 3, no pairs, gives [0, 1, 2].
    • numCourses = 3, [[0, 2], [1, 2]] gives [2, 0, 1].
    • numCourses = 3, [[2, 0], [1, 0]] gives [0, 1, 2].
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free