Start learning free

LESSON 478 OF 539 · 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 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 List 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 List.

 

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 19
    Ctrl + Enter to check

    C# · GRAPHS

    Course Schedule Ii in C#, explained

    Problem overview

    There are numCourses courses labelled 0 to numCourses - 1. Each pair [a, b] in prerequisites means course b must be taken before course a. The method FindOrder returns an order in which all the courses can be taken, as a List<int>. A course is ready when all its prerequisites have been taken, and whenever several courses are ready, the one with the smallest number goes next. That rule makes the answer unique. If taking every course is impossible, the method returns an empty list.

    In the first example, numCourses = 4 and prerequisites = {{1,0},{2,0},{3,1},{3,2}}. Only course 0 is ready at first. After it, courses 1 and 2 are ready and 1 comes first, then 2, and finally 3. The answer is [0, 1, 2, 3]. In the second example, numCourses = 5 and prerequisites = {{1,4},{0,4},{2,3}}, the answer is [3, 2, 4, 0, 1]: course 3 is smaller than 4, and once it is taken course 2 is ready and smaller than 4. In the third example, courses 1 and 2 need each other, so the answer is [].

    The lesson asks for O((V + E) log V) time and O(V + E) extra space, where V is the number of courses and E the number of pairs.

    Approach: Kahn's algorithm with a priority queue

    Treat each pair as an arrow from the course to take first to the course that needs it. A valid order takes every course after all the arrows pointing into it, which is called a topological order. Kahn's algorithm builds one:

    • Count, for every course, how many prerequisites are still untaken. This is its in-degree.
    • Take any course whose count is 0. Taking it removes its outgoing arrows: each course waiting for it has its count lowered by one.
    • A course whose count reaches 0 is now ready. Repeat until nothing is ready.

    The usual version keeps the ready courses in a plain queue. Here the smallest ready course must always be taken first, so they wait in a min-heap instead, which in C# is a PriorityQueue. Each course enters the heap once and leaves it once, at O(log V) each, and each pair is looked at once. Scanning all the courses for the smallest ready one before every pick would cost O(V²).

    The heap matters. With a plain queue, the second example comes out as [3, 4, 2, 1, 0]: it is a valid order, but it takes 4 while the smaller course 2 is ready, so it is not the answer.

    If fewer than numCourses courses get taken, the rest wait on a cycle, or on a course that does, and their counts never reach 0. In that case no order exists.

    Step by step

    1. Create graph, a List<List<int>>. Entry graph[b] will list the courses that wait for course b.
    2. Loop over the courses and add a new empty List<int> for each. The inner lists are separate objects, so they are added one at a time.
    3. Create inDegree, an int array of size numCourses, filled with zeros. inDegree[a] counts the prerequisites of course a not yet taken.
    4. Loop over prerequisites with foreach. For each pair, add prereq[0] to graph[prereq[1]] (the course to take first points at the course that waits) and add one to inDegree[prereq[0]].
    5. Create the heap, a PriorityQueue<int, int> named ready. Each element comes with a priority, and Dequeue removes the element with the smallest priority.
    6. Enqueue every course with in-degree 0, using the course number as both element and priority: ready.Enqueue(course, course). These are the places any valid order can start.
    7. Create order, an empty List<int> for the courses in the order they are taken.
    8. Loop while ready.Count is above 0. Dequeue the smallest ready course and add it to order. Every course in the heap has all its prerequisites taken, so this is safe.
    9. Visit the courses waiting for it. For each nextCourse in graph[course], subtract one from its in-degree. If the result is 0, enqueue it: its last prerequisite was just taken. A count reaches 0 only once, so each course enters the heap at most once.
    10. After the loop, return a new empty list if order.Count < numCourses. Otherwise return order.

    The solution in C#

    class Solution {
        public List<int> FindOrder(int numCourses, int[][] prerequisites) {
            List<List<int>> graph = new List<List<int>>();
            for (int i = 0; i < numCourses; i++) {
                graph.Add(new List<int>());
            }
            int[] inDegree = new int[numCourses];
            foreach (int[] prereq in prerequisites) {
                graph[prereq[1]].Add(prereq[0]);
                inDegree[prereq[0]]++;
            }
            PriorityQueue<int, int> ready = new PriorityQueue<int, int>();
            for (int course = 0; course < numCourses; course++) {
                if (inDegree[course] == 0) {
                    ready.Enqueue(course, course);
                }
            }
            List<int> order = new List<int>();
            while (ready.Count > 0) {
                int course = ready.Dequeue();
                order.Add(course);
                foreach (int nextCourse in graph[course]) {
                    inDegree[nextCourse]--;
                    if (inDegree[nextCourse] == 0) {
                        ready.Enqueue(nextCourse, nextCourse);
                    }
                }
            }
            if (order.Count < numCourses) {
                return new List<int>();
            }
            return order;
        }
    }
    

    The C# details worth knowing:

    • PriorityQueue<TElement, TPriority>. It is in System.Collections.Generic, available since .NET 6. It is a min-heap: Dequeue returns the element with the smallest priority, and calling it on an empty queue throws an InvalidOperationException. The loop condition rules that out. It takes two type arguments, even when, as here, the element and the priority are the same number.
    • A list of lists. List<List<int>> starts empty, and indexing it with graph[b] works only after the inner list has been added. That is why the first loop adds one list per course before any pair is read.
    • Value types in the array. inDegree[prereq[0]]++ changes the array element in place. A new int[] is filled with zeros, so no initialization loop is needed.
    • Same name, separate scopes. The loop variable course in the for loop and the int course inside the while body don't collide, because the first exists only inside its for.
    • Returning an empty list. A cycle gives new List<int>(), not null. A caller can then use Count or a foreach without a null check.
    • The input array is not changed. The method only reads prerequisites.

    Complexity

    Time: O((V + E) log V). Building the graph is O(V + E). Each course is enqueued and dequeued once, at O(log V) each. Each pair is looked at once, when its first course is taken. With 100,000 courses chained one after another, the method returns all of them at once.

    Space: O(V + E). The graph holds V lists with E entries in total, plus the in-degree array, the heap and the order, each at most V items.

    Edge cases and common mistakes

    • No prerequisites. With numCourses = 3 and no pairs, the answer is [0, 1, 2]: everything is ready, and the heap gives them smallest first. A single course with no pairs returns [0].
    • Reading the pair backwards. In [a, b], course b comes first. For {{0, 1}} with two courses, the answer is [1, 0].
    • A cycle. {{0,1},{1,0}} gives [], because no course starts with in-degree 0 and the heap is empty from the beginning. A cycle in only part of the graph, as in the third example, also gives [], even though course 0 could be taken.
    • Using a plain queue. The result may be a valid topological order but not the one with the smallest ready course first.
    • Returning a partial order. Check order.Count < numCourses and return an empty list instead.
    • Swapping the arrow direction. Adding prereq[1] to graph[prereq[0]] reverses every dependency, and the order comes out backwards.
    • Forgetting that counts reach 0 once. Enqueuing a course on any decrement, rather than only when its count reaches 0, would add it several times.

    Testing

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