-
Notifications
You must be signed in to change notification settings - Fork 0
/
course_schedule.py
147 lines (116 loc) · 5.87 KB
/
course_schedule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env python3
# Course Schedule
#
# https://leetcode.com/problems/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.
from collections import defaultdict
from typing import List
def test():
"""
Run `pytest <this-file>`.
"""
def test_algo(algo):
assert algo(numCourses=2, prerequisites=[[1, 0]]) == True
assert algo(numCourses=2, prerequisites=[[1, 0], [0, 1]]) == False
# None of the courses have depend on other courses.
assert algo(numCourses=42, prerequisites=[]) == True
# Course 2 depends on course 0 "twice", once directly and once transitively through course 1. This is fine.
assert algo(numCourses=3, prerequisites=[[1, 0], [2, 0], [2, 1]]) == True
# Test all different algorithms/implementations
solution = Solution()
for algo in [solution.dfs, solution.dfs_optimized]:
test_algo(algo)
class Solution:
def dfs(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
"""
Approach: DFS.
Idea: Try every course as "starting course from which we visit other courses that have it as prerequisite", which will eventually visit all courses. If we see a cycle at any point, it's impossible to finish all courses.
Time: O(V + E): We only visit every node/course once (O(V)) and every edge/prerequisite/dependency once (O(E)).
Space: O(V + E): We store all courses in a hashset (O(V)) and all dependencies in a hashmap (O(E)).
Leetcode: 97 ms runtime, 18.87 MB memory
"""
# It is possible to finish all courses, if **all** of the following hold:
# - There are no cycles in the dependency graph.
courses = set()
# Map each course x to all of its dependents, i.e. all courses that depend on x/have x as a prerequisite.
dependents = defaultdict(lambda: [])
for (course, prerequisite) in prerequisites:
dependents[prerequisite].append(course)
courses.add(course)
courses.add(prerequisite)
visited = set()
current_path_stack = set()
def explore_from(node: int):
"""DFS."""
visited.add(node)
current_path_stack.add(node)
for neighbour in dependents[node]:
if neighbour in visited:
# If we see a visited node in our current path again, we have encountered a cycle.
if neighbour in current_path_stack:
raise Exception("found a cycle in the graph")
else:
explore_from(neighbour)
current_path_stack.remove(node)
# Try to visit all courses/nodes.
for starting_course in courses:
# Only explore from this course/node if we haven't yet visited it.
if starting_course not in visited:
try:
explore_from(starting_course)
except Exception:
# If the dependents graph contains a cycle, we can never finish all courses.
return False
# We will always have visited all courses at this point, since we tried each node as starting node and encountered no cycles.
return True
def dfs_optimized(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
"""
Approach: DFS.
Idea: Try every course as "starting course from which we visit other courses that have it as prerequisite", which will eventually visit all courses. If we see a cycle at any point, it's impossible to finish all courses.
Time: O(V + E): We only visit every node/course once (O(V)) and every edge/prerequisite/dependency once (O(E)).
Space: O(E): We store all dependencies in a hashmap (O(E)).
Leetcode: 96 ms runtime, 18.20 MB memory
"""
# It is possible to finish all courses, if **all** of the following hold:
# - There are no cycles in the dependency graph.
def courses():
for course in range(0, numCourses):
yield course
# Map each course x to all of its dependents, i.e. all courses that depend on x/have x as a prerequisite.
dependents = defaultdict(lambda: [])
for (course, prerequisite) in prerequisites:
dependents[prerequisite].append(course)
visited = set()
current_path_stack = set()
def explore_from(node: int):
"""DFS."""
visited.add(node)
current_path_stack.add(node)
for neighbour in dependents[node]:
if neighbour in visited:
# If we see a visited node in our current path again, we have encountered a cycle.
if neighbour in current_path_stack:
raise Exception("found a cycle in the graph")
else:
explore_from(neighbour)
current_path_stack.remove(node)
# Try to visit all courses/nodes.
for starting_course in courses():
# Only explore from this course/node if we haven't yet visited it.
if starting_course not in visited:
try:
explore_from(starting_course)
except Exception:
# If the dependents graph contains a cycle, we can never finish all courses.
return False
# We will always have visited all courses at this point, since we tried each node as starting node and encountered no cycles.
return True