Start learning free

LESSON 479 OF 540 · GRAPHS

Course Schedule II in Kotlin: 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 an ArrayList 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 ArrayList.

 

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

    KOTLIN · GRAPHS

    Course Schedule Ii in Kotlin, explained

    Problem overview

    There are numCourses courses numbered 0 to numCourses - 1. Each pair [a, b] in prerequisites means course b must be taken before course a. findOrder returns an order that takes all the courses, as a List<Int>. A course is ready when all its prerequisites are taken, and whenever several 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 list.

    For numCourses = 4 and prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]] the answer is [0, 1, 2, 3]. Only course 0 is ready at first. After it, courses 1 and 2 are ready and 1 is smaller. Course 3 comes last. The order [0, 2, 1, 3] respects every prerequisite too, but it takes 2 while the smaller course 1 is ready, so it is not the answer.

    For [[1, 0], [2, 1], [1, 2]] with 3 courses, courses 1 and 2 wait for each other. That is a cycle, so the answer is [].

    Approach

    View each pair as an arrow from the course to take first to the course that needs it. Then you want an order in which every arrow points forward: a topological order. Kahn's algorithm builds one:

    1. Count, for each course, how many prerequisites it still waits for: its in-degree.
    2. Courses with count 0 are ready. Take one.
    3. Taking a course removes its arrows: each course that waited for it gets its count lowered by 1. A course whose count reaches 0 is now ready.
    4. Repeat until nothing is ready.

    The usual version keeps the ready courses in a plain queue. Here the rule says the smallest ready course goes first, so they wait in a min-heap instead. With a plain queue, the pairs [[1, 4], [0, 4], [2, 3]] with 5 courses would give [3, 4, 2, 1, 0], because course 1 was added to the queue before 0. The heap gives [3, 2, 4, 0, 1]: after 3, courses 2 and 4 are ready and 2 is smaller, and after 4 the courses 0 and 1 are ready, smaller first.

    If fewer than numCourses courses get taken, the rest wait on a cycle, or on a course that is on one, and no order exists. Searching all courses for the smallest ready one before every pick would cost O(V²). The heap makes each pick O(log V), so the total is O((V + E) log V) for V courses and E pairs.

    Step by step

    1. The graph. Array(numCourses) { mutableListOf<Int>() } gives every course its own empty list, because the lambda runs once per course. graph[b] holds the courses that wait for course b.
    2. The counts. IntArray(numCourses) starts as zeros. inDegree[a] counts the prerequisites of course a that are not taken yet.
    3. Read the pairs. For each prereq, add prereq[0] to graph[prereq[1]] (course prereq[1] comes first), and do inDegree[prereq[0]]++.
    4. The heap. PriorityQueue<Int>() is a min-heap: add puts a value in and poll() removes and returns the smallest, each in O(log n).
    5. Seed it. Loop over 0 until numCourses and add every course with inDegree[course] == 0. If none is free, every course waits for another and none can be taken.
    6. The result list. order is an empty mutableListOf<Int>(). Its size also counts the courses taken so far.
    7. The main loop. While ready.isNotEmpty(), take the smallest course with poll(), append it to order, and visit graph[course]. This is the key line: of all the ready courses, the smallest number comes next.
    8. Release the waiting courses. For each nextCourse, do inDegree[nextCourse]--. When it reaches 0, add it to ready. A count reaches 0 only once, so each course enters the heap at most once.
    9. Check for a cycle. If order.size < numCourses, return listOf(). Otherwise return order.

    The solution in Kotlin

    class Solution {
        fun findOrder(numCourses: Int, prerequisites: Array<IntArray>): List<Int> {
            val graph = Array(numCourses) { mutableListOf<Int>() }
            val inDegree = IntArray(numCourses)
            for (prereq in prerequisites) {
                graph[prereq[1]].add(prereq[0])
                inDegree[prereq[0]]++
            }
            val ready = PriorityQueue<Int>()
            for (course in 0 until numCourses) {
                if (inDegree[course] == 0) {
                    ready.add(course)
                }
            }
            val order = mutableListOf<Int>()
            while (ready.isNotEmpty()) {
                val course = ready.poll()
                order.add(course)
                for (nextCourse in graph[course]) {
                    inDegree[nextCourse]--
                    if (inDegree[nextCourse] == 0) {
                        ready.add(nextCourse)
                    }
                }
            }
            if (order.size < numCourses) {
                return listOf()
            }
            return order
        }
    }
    

    Kotlin details worth knowing:

    • PriorityQueue comes from Java. It is java.util.PriorityQueue, which Kotlin on the JVM can use directly. In a file of your own, add import java.util.PriorityQueue at the top.
    • One list per course. The lambda creates a separate list per course. Filling the array with one shared list would mix all the courses' neighbours.
    • inDegree[x]++ works on array elements and changes the element in place.
    • The empty result. listOf() infers List<Int> from the return type. The function returns a read-only List<Int>, while order is a MutableList<Int> that is returned as a List.
    • poll() returns null on an empty queue. It is safe here because the loop condition checks isNotEmpty() first.

    Complexity

    Time: each course is pushed to the heap and popped once, O(log V) each, and each pair is looked at once when its first course is taken: O((V + E) log V).

    Space: the graph holds E entries, plus the arrays and the heap with V entries: O(V + E).

    Edge cases and common mistakes

    • No prerequisites. With numCourses = 3 and no pairs, every course is ready and the answer is [0, 1, 2]. With one course the answer is [0].
    • A cycle. [[0, 1], [1, 0]] with 2 courses leaves nothing ready at the start, so order stays empty and the function returns [].
    • A cycle behind other courses. Courses outside the cycle are still taken, but the final size check catches the missing ones. In the third example, course 0 is taken and the answer is still [].
    • Arrow direction. The pair [a, b] means b first. Using graph[a].add(b) reverses all the arrows. With [[0, 1]] the correct answer is [1, 0].
    • A plain queue. It gives a valid topological order, but not the smallest-first order the task asks for.
    • Returning the partial order. On a cycle the function returns an empty list, not the courses taken so far.

    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 pairs                   gives [0, 1, 2]
    2 courses, [[0,1]]                    gives [1, 0]
    2 courses, [[0,1],[1,0]]              gives []
    
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free