{x}
blog image

Course Schedule

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

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

Return true if you can finish all courses. Otherwise, return false.

 

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are a total of 2 courses to take. 
To take course 1 you should have finished course 0. So it is possible.

Example 2:

Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take. 
To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

 

Constraints:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • All the pairs prerequisites[i] are unique.

Solution Explanation: Course Schedule

This problem asks whether it's possible to finish all courses given prerequisites. We can model this as a directed graph where courses are nodes and prerequisites are edges. A course a has an edge to course b if b must be taken before a. The problem then becomes determining if there's a cycle in this directed graph. A cycle indicates an impossible schedule because courses depend on each other circularly.

The most efficient solution uses topological sorting. Topological sorting arranges nodes in a directed acyclic graph (DAG) such that for every directed edge from node A to node B, node A appears before node B in the ordering. If a cycle exists, topological sorting is impossible.

Algorithm:

  1. Graph Representation: Create an adjacency list g to represent the course dependencies. g[i] contains a list of courses that depend on course i. Also, create an indeg array to store the in-degree (number of incoming edges) of each course.

  2. Initialization: Populate g and indeg using the prerequisites array. The in-degree of a course represents the number of courses that must be taken before it.

  3. Queue: Initialize a queue q with all courses having an in-degree of 0 (courses with no prerequisites).

  4. Topological Sort: Iterate while the queue is not empty:

    • Dequeue a course i.
    • Decrement the in-degree of all courses that depend on i (courses in g[i]).
    • If the in-degree of a dependent course becomes 0, add it to the queue.
  5. Cycle Detection: After the loop, if numCourses is 0, it means all courses have been processed, implying no cycles. Otherwise, a cycle exists.

Time Complexity: O(V + E), where V is the number of courses (nodes) and E is the number of prerequisites (edges). This is because we visit each node and edge once.

Space Complexity: O(V + E), due to the adjacency list g and indeg array.

Code Examples (Python):

from collections import defaultdict, deque
 
def canFinish(numCourses, prerequisites):
    """
    Determines if all courses can be finished given prerequisites.
 
    Args:
        numCourses: The total number of courses.
        prerequisites: A list of prerequisites, where each element is a list [a, b] indicating that course a requires course b.
 
    Returns:
        True if all courses can be finished, False otherwise.
    """
 
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    for a, b in prerequisites:
        graph[b].append(a)  # Course 'a' depends on course 'b'
        in_degree[a] += 1
 
    queue = deque([i for i, degree in enumerate(in_degree) if degree == 0])
    count = 0  # Count of processed courses
 
    while queue:
        course = queue.popleft()
        count += 1
        for neighbor in graph[course]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
 
    return count == numCourses # if all courses were processed, no cycles
 

The code in other languages follows the same algorithm, only differing in syntax and data structure implementations. The core logic remains consistent across all languages presented.