Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
- uses: actions/checkout@v5
- uses: actions/cache@v4
with:
path: |
~/.cache/pre-commit
Expand Down
3 changes: 3 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
* [Test Find Duplicate](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/fast_and_slow/find_duplicate/test_find_duplicate.py)
* Happy Number
* [Test Happy Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/fast_and_slow/happy_number/test_happy_number.py)
* Graphs
* Course Schedule
* [Test Course Schedule](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/course_schedule/test_course_schedule.py)
* Greedy
* Min Arrows
* [Test Find Min Arrows](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/min_arrows/test_find_min_arrows.py)
Expand Down
90 changes: 79 additions & 11 deletions algorithms/graphs/course_schedule/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,55 @@ should take to finish all courses.
If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example 1:

```text
Input: numCourses = 2, prerequisites = [[1,0]]
Output: [0,1]
Explanation: There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct
course order is [0,1]. Example 2:
course order is [0,1].
```

Example 2:
```text
Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0,2,1,3]
Explanation: There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2.
Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another
correct ordering is [0,2,1,3]. Example 3:
correct ordering is [0,2,1,3].
```

Example 3:
```text
Input: numCourses = 1, prerequisites = []
Output: [0]
```

Example 4:
```text
Input: numCourses = 3, prerequisites = [[1,0],[2,1]]
Output: [0,1,2]
```

Example 5:
```text
Input: numCourses = 3, prerequisites = [[1,0],[2,1],[1,2]]
Output: []
```

Example 6:
```text
Input: numCourses = 5, prerequisites = [[1,0],[2,1],[4,3]]
Output: [0,1,2,3,4]
```

Constraints:

1 <= numCourses <= 2000 0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2 0 <= ai, bi < numCourses ai != bi All the pairs [ai, bi] are distinct.
- 1 <= numCourses <= 2000
- 0 <= prerequisites.length <= numCourses * (numCourses - 1)
- prerequisites[i].length == 2 0 <= ai, bi < numCourses ai != bi All the pairs [ai, bi] are distinct.

---

# Solution Breakdown - Using Depth First Search
## Solution 1 Breakdown (Using Depth First Search)

Suppose we are at a node in our graph during the depth first traversal. Let's call this node A.

Expand All @@ -56,7 +82,7 @@ Initialize a stack S that will contain the topologically sorted order of the cou
adjacency list using the edge pairs given in the input. An important thing to note about the input for the problem is
that a pair such as [a, b] represents that the course b needs to be taken in order to do the course a. This implies an
edge of the form b ➔ a. Please take note of this when implementing the algorithm. For each of the nodes in our graph, we
will run a depth first search in case that node was not already visited in some other node's DFS traversal. Suppose we
will run a depth-first search in case that node was not already visited in some other node's DFS traversal. Suppose we
are executing the depth first search for a node N. We will recursively traverse all of the neighbors of node N which
have not been processed before. Once the processing of all the neighbors is done, we will add the node N to the stack.
We are making use of a stack to simulate the ordering we need. When we add the node N to the stack, all the nodes that
Expand All @@ -65,15 +91,57 @@ processed, we will simply return the nodes as they are present in the stack from

## Complexity Analysis

Time Complexity: O(V+E) where V represents the number of vertices and E represents the number of edges.
Essentially we iterate through each node and each vertex in the graph once and only once.
### Time Complexity

O(V+E) where V represents the number of vertices and E represents the number of edges. Essentially we iterate through
each node and each vertex in the graph once and only once.

Space Complexity: O(V+E).
### Space Complexity:

O(V+E).

We use the adjacency list to represent our graph initially. The space occupied is defined by the number of edges because
for each node as the key, we have all its adjacent nodes in the form of a list as the value. Hence, O(E)

Additionally, we apply recursion in our algorithm, which in worst case will incur O(E) extra space in the function
call stack.

To sum up, the overall space complexity is O(V+E).
To sum up, the overall space complexity is O(V+E).

---

## Solution 2 Breakdown

Initialize the hash map with the vertices and their children. We’ll use another hash map to keep track of the number of
in-degrees of each vertex. Then we’ll find the source vertex (with 0 in-degree) and increment the counter. Retrieve the
source node’s children and add them to the queue. Decrement the in-degrees of the retrieved children. We’ll check
whether the in-degree of the child vertex becomes equal to zero, and we increment the counter. Repeat the process until
the queue is empty.

> Note: The in-degree is the number of edges coming into a vertex in a directed graph.

The primary purpose of finding a vertex with 0 in-degree is to find a course with a pre-requisite count of 0. When we
take a course, say a (that is the pre-requisite of another course, say b), we’ll decrement the in-degree of b by 1, and
if the in-degree count becomes 0, we can say that the b’s pre-requisites have been completed.

The images below illustrate the algorithm above, where num_courses = 6:

![Solution_2_slide_1](./images/solution_2_slide_1.png)
![Solution_2_slide_2](./images/solution_2_slide_2.png)
![Solution_2_slide_3](./images/solution_2_slide_3.png)
![Solution_2_slide_4](./images/solution_2_slide_4.png)
![Solution_2_slide_5](./images/solution_2_slide_5.png)
![Solution_2_slide_6](./images/solution_2_slide_6.png)
![Solution_2_slide_7](./images/solution_2_slide_7.png)
![Solution_2_slide_8](./images/solution_2_slide_8.png)
![Solution_2_slide_9](./images/solution_2_slide_9.png)

### Time Complexity

In the algorithm above, each course will become a source only once, and each edge will be accessed and removed once.
Therefore, the above algorithm’s time complexity will be O(V+E), where V is the total number of vertices and E is the
total number of edges in the graph.

### Space Complexity

The space complexity will be O(V+E) because we’re storing all of the edges for each vertex in an adjacency list.
90 changes: 88 additions & 2 deletions algorithms/graphs/course_schedule/__init__.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,127 @@
from collections import defaultdict
from typing import List
from collections import defaultdict, deque
from typing import List, Dict, Deque

WHITE = 1
GRAY = 2
BLACK = 3


def find_order(num_courses: int, prerequisites: List[List[int]]) -> List[int]:
"""
This function finds the topological sorted order of the courses given the prerequisites.

Args:
num_courses (int): The total number of courses.
prerequisites (List[List[int]]): A list of tuples where each tuple contains the source and destination of the prerequisite.

Returns:
List[int]: A list of the courses in the topological sorted order. If there is no valid order, return an empty list.

"""
adjacency_list = defaultdict(list)

# Populate the adjacency list with the prerequisites
for destination, source in prerequisites:
adjacency_list[source].append(destination)

topological_sorted_order = []
is_possible = True

# Use a dictionary to keep track of the color of each node
color = {k: WHITE for k in range(num_courses)}

def dfs(node):
"""
This function performs a depth-first search on the graph.

Args:
node (int): The current node being visited.

Returns:
None

"""
nonlocal is_possible

# If there is no valid order, return immediately
if not is_possible:
return

# Mark the current node as gray
color[node] = GRAY

# If the current node has any neighbours, visit them
if node in adjacency_list:
for neighbour in adjacency_list[node]:
# If the neighbour is white, visit it
if color[neighbour] == WHITE:
dfs(neighbour)
# If the neighbour is gray, there is no valid order
elif color[neighbour] == GRAY:
is_possible = False

# Mark the current node as black
color[node] = BLACK
topological_sorted_order.append(node)

# Visit all the nodes
for vertex in range(num_courses):
if color[vertex] == WHITE:
dfs(vertex)

# If there is no valid order, return an empty list
return topological_sorted_order[::-1] if is_possible else []


def can_finish(
num_courses: int,
prerequisites: List[List[int]]
) -> bool:
"""
Determines if there is a valid order of courses such that
all prerequisites are satisfied.

Args:
num_courses (int): The total number of courses.
prerequisites (List[List[int]]): A list of tuples where each tuple contains the source and destination of the prerequisite.

Returns:
bool: True if there is a valid order, False otherwise.
"""
counter: int = 0
if num_courses <= 0:
return True

# Initialize the in-degree of all nodes to 0
in_degree: Dict[int, int] = {i: 0 for i in range(num_courses)}
# Initialize an adjacency list to store the graph
graph: Dict[int, List[int]] = {i: [] for i in range(num_courses)}

# Populate the adjacency list and the in-degree of all nodes
for child, parent in prerequisites:
if parent in graph:
graph[parent].append(child)
else:
graph[parent] = [child]
if child in in_degree:
in_degree[child] += 1
else:
in_degree[child] = 1

# Initialize a queue to store all nodes with an in-degree of 0
sources: Deque[int] = deque()
for key in in_degree:
if in_degree[key] == 0:
sources.append(key)

# Perform a BFS traversal of the graph
while sources:
course: int = sources.popleft()
counter += 1
for child in graph[course]:
in_degree[child] -= 1
if in_degree[child] == 0:
sources.append(child)

# If all nodes have been visited, return True
return counter == num_courses
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions algorithms/graphs/course_schedule/test_course_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import unittest
from . import find_order, can_finish


class FindOrderTestCase(unittest.TestCase):
def test_1(self):
num_courses = 2
prerequisites = [[1,0]]
expected = [0, 1]
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)

def test_2(self):
num_courses = 4
prerequisites = [[1,0],[2,0],[3,1],[3,2]]
expected = [0,2,1,3]
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)


def test_3(self):
num_courses = 1
prerequisites = []
expected = [0]
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)

def test_4(self):
num_courses = 3
prerequisites = [[1,0],[2,1]]
expected = [0,1,2]
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)

def test_5(self):
num_courses = 3
prerequisites = [[1,0],[2,1],[1,2]]
expected = []
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)

def test_6(self):
num_courses = 3
prerequisites = [[1,0],[2,1],[4,3]]
expected = [0,1,2] # or [0,1,2,3,4]
actual = find_order(num_courses=num_courses, prerequisites=prerequisites)
self.assertEqual(expected, actual)


class CanFinishTestCases(unittest.TestCase):
def test_1(self):
num_courses = 2
prerequisites = [[1,0]]
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertTrue(actual)

def test_2(self):
num_courses = 4
prerequisites = [[1,0],[2,0],[3,1],[3,2]]
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertTrue(actual)

def test_3(self):
num_courses = 1
prerequisites = []
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertTrue(actual)

def test_4(self):
num_courses = 3
prerequisites = [[1,0],[2,1]]
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertTrue(actual)

def test_5(self):
num_courses = 3
prerequisites = [[1,0],[2,1],[1,2]]
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertFalse(actual)

def test_6(self):
num_courses = 3
prerequisites = [[1,0],[2,1],[4,3]]
actual = can_finish(num_courses=num_courses, prerequisites=prerequisites)
self.assertTrue(actual)


if __name__ == '__main__':
unittest.main()
Loading