From 486a847760957b06ebad778f8504c891c30aeb91 Mon Sep 17 00:00:00 2001 From: miguelhx Date: Mon, 25 Oct 2021 18:00:35 -0700 Subject: [PATCH 1/6] Graph boilerplate code --- Python/chapter04/p07_build_order/miguelHx.py | 182 +++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 Python/chapter04/p07_build_order/miguelHx.py diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py new file mode 100644 index 00000000..2ac89537 --- /dev/null +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -0,0 +1,182 @@ +"""Python Version 3.9.2 +4.7 - Build Order: +You are given a list of projects and a list of dependencies +(which is a list of pairs of projects, where the second project is dependent on the first project). +All of a project's dependencies must be built before the project is. +Find a build order that will allow the projects to be built. If there is no +valid build order, return an error. +EXAMPLE +Input: + projects: a, b, c, d, e, f + dependencies: (a, d), (f, b), (b, d), (f, a), (d, c) +Output: + f, e, a, b, d, c +""" +import unittest + +from collections import deque +from dataclasses import dataclass +from typing import List, Deque, Set + + +@dataclass +class Graph: + nodes: 'List[Node]' + + def print_graph(self): + for node in self.nodes: + node.print_children() + + +@dataclass +class Node: + id: int + children: 'List[Node]' + + def add_child(self, *nodes: 'Node'): + for node in nodes: + self.children.append(node) + + def children_as_str(self) -> str: + return ', '.join(str(child.id) for child in self.children) + + def print_children(self): + logging.debug('Adjacency list for node %s: %s', self.id, self.children_as_str()) + + def __str__(self): + return f'Node ({self.id}), children: {self.children_as_str()}' + +def bfs_search_exhaustive(root: Node) -> List[int]: + """Simple BFS. + takes in a root, returns a list + of ids of the sequence of visited + nodes. Goes through entire graph. + Args: + root (Node): starting node + Returns: + List[int]: List[int]: list of node IDs (i.e. [0, 1, 4]) + """ + visited_list: List[int] = [root.id] + visited: Set[int] = set([root.id]) + queue: Deque[Node] = deque([root]) + while queue: + node = queue.popleft() + # print(f'Visiting node ({node.id})') + for n in node.children: + if n.id not in visited: + queue.append(n) + visited_list.append(n.id) + visited.add(n.id) + return visited_list + + +def bfs_search_for_dest(root: Node, dest: Node) -> List[int]: + """Simple BFS. + takes in a root, returns a list + of ids of the sequence of visited + nodes. Stops at destination node + Args: + root (Node): starting node + Returns: + List[int]: List[int]: list of node IDs (i.e. [0, 1, 4]) + """ + visited_list: List[int] = [root.id] + visited: Set[int] = set([root.id]) + queue: Deque[Node] = deque([root]) + while queue: + node = queue.popleft() + # print(f'Visiting node ({node.id})') + for n in node.children: + if n.id not in visited: + queue.append(n) + visited_list.append(n.id) + visited.add(n.id) + if n.id == dest.id: + # done searching + return visited_list + return visited_list + +def route_between_nodes(src: Node, dest: Node) -> bool: + """This function will return true if a path + is found between two nodes, false otherwise. + The idea is to perform a breadth first search + from src to dest. After obtaining a list of + nodes visited, we simply check to see if destination + node id is in there. + Runtime Complexity: + O(V + E) where V represents the number of + nodes in the graph and E represents the number + of edges in this graph. + Space Complexity: + O(V) where V represents the number of existing nodes + in the graph. + Args: + src (Node): from node + dest (Node): destination node + Returns: + bool: whether a path between src and dest exists + """ + ids_visited: List[int] = bfs_search_for_dest(src, dest) + return dest.id in ids_visited + + +class TestRouteBetweenNodes(unittest.TestCase): + def test_route_between_nodes(self): + n0 = Node(0, []) + n1 = Node(1, []) + n2 = Node(2, []) + n3 = Node(3, []) + n4 = Node(4, []) + n5 = Node(5, []) + n0.add_child(n1, n4, n5) + n1.add_child(n3, n4) + n2.add_child(n1) + n3.add_child(n2, n4) + # must remember to reset node visited properties + # before each fresh run + g = Graph([n0, n1, n2, n3, n4, n5]) + # There is a route from node 0 to node 2 + self.assertTrue(route_between_nodes(n0, n2)) + # No route between node 1 and node 0 + self.assertFalse(route_between_nodes(n1, n0)) + # There is a route from node 2 to node 3 + self.assertTrue(route_between_nodes(n2, n3)) + +class TestMyGraphSearch(unittest.TestCase): + + def test_basic_graph_creation(self): + n0 = Node(0, []) + n1 = Node(1, []) + n2 = Node(2, []) + n3 = Node(3, []) + n4 = Node(4, []) + n5 = Node(5, []) + n6 = Node(6, []) + n0.add_child(n1) + n1.add_child(n2) + n2.add_child(n0, n3) + n3.add_child(n2) + n4.add_child(n6) + n5.add_child(n4) + n6.add_child(n5) + nodes = [n0, n1, n2, n3, n4, n5, n6] + g = Graph(nodes) + # g.print_graph() + + def test_basic_breadth_first_search_exhaustive(self): + n0 = Node(0, []) + n1 = Node(1, []) + n2 = Node(2, []) + n3 = Node(3, []) + n4 = Node(4, []) + n5 = Node(5, []) + n0.add_child(n1, n4, n5) + n1.add_child(n3, n4) + n2.add_child(n1) + n3.add_child(n2, n4) + result: List[int] = bfs_search_exhaustive(n0) + self.assertEqual(result, [0, 1, 4, 5, 3, 2]) + + +if __name__ == '__main__': + unittest.main() From 7904755821e2f6a4185dfeb06c8c5dd713bbe7f4 Mon Sep 17 00:00:00 2001 From: miguelhx Date: Mon, 25 Oct 2021 18:09:17 -0700 Subject: [PATCH 2/6] Set up build_order function signature --- Python/chapter04/p07_build_order/miguelHx.py | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py index 2ac89537..005101ae 100644 --- a/Python/chapter04/p07_build_order/miguelHx.py +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -16,7 +16,7 @@ from collections import deque from dataclasses import dataclass -from typing import List, Deque, Set +from typing import List, Deque, Set, Tuple @dataclass @@ -120,6 +120,30 @@ def route_between_nodes(src: Node, dest: Node) -> bool: return dest.id in ids_visited +def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> List[str]: + """Given a list of projects and dependencies, + this function will find a build order that will + allow the projects to be build given the dependencies. + If there is no valid build order, an error will be raised. + + EXAMPLE + Input: + projects: a, b, c, d, e, f + dependencies: (a, d), (f, b), (b, d), (f, a), (d, c) + Output: + f, e, a, b, d, c + + Args: + projects (List[str]): a list of projects + dependencies (List[Tuple[str, str]]): + a list of pairs of dependencies (2nd project is dependent on 1st) + + Returns: + List[str]: a valid build order + """ + pass + + class TestRouteBetweenNodes(unittest.TestCase): def test_route_between_nodes(self): n0 = Node(0, []) From b5fff81e06b88f526e10426a51a0c50dda339d1e Mon Sep 17 00:00:00 2001 From: miguelhx Date: Mon, 25 Oct 2021 21:11:17 -0700 Subject: [PATCH 3/6] Make progress on build_order and set up test case --- Python/chapter04/p07_build_order/miguelHx.py | 67 +++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py index 005101ae..fbcfb6b6 100644 --- a/Python/chapter04/p07_build_order/miguelHx.py +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -41,7 +41,7 @@ def children_as_str(self) -> str: return ', '.join(str(child.id) for child in self.children) def print_children(self): - logging.debug('Adjacency list for node %s: %s', self.id, self.children_as_str()) + print('Adjacency list for node {}: {}'.format(self.id, self.children_as_str())) def __str__(self): return f'Node ({self.id}), children: {self.children_as_str()}' @@ -120,11 +120,40 @@ def route_between_nodes(src: Node, dest: Node) -> bool: return dest.id in ids_visited +@dataclass +class DependencyNode: + id: str + children: 'List[DependencyNode]' + + def add_child(self, *nodes: 'DependencyNode'): + for node in nodes: + self.children.append(node) + + def children_as_str(self) -> str: + return ', '.join(str(child.id) for child in self.children) + + def print_children(self): + print('Adjacency list for node {}: {}'.format(self.id, self.children_as_str())) + + def __str__(self): + return f'Node ({self.id}), children: {self.children_as_str()}' + +@dataclass +class DependencyGraph: + nodes: 'List[DependencyNode]' + + def print_graph(self): + for node in self.nodes: + node.print_children() + + def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> List[str]: """Given a list of projects and dependencies, this function will find a build order that will allow the projects to be build given the dependencies. If there is no valid build order, an error will be raised. + All of a project's dependencies must be built before the project + is. EXAMPLE Input: @@ -141,8 +170,27 @@ def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> Lis Returns: List[str]: a valid build order """ - pass + # 0. define output + output: List[str] = [] + # 1. build a dependency graph + project_to_node_map = {} + for p in projects: + project_to_node_map[p] = DependencyNode(p, []) + for d in dependencies: + p1 = d[0] + p2 = d[1] + project_to_node_map[p2].add_child(project_to_node_map[p1]) + nodes = [] + for node in project_to_node_map.values(): + nodes.append(node) + g = DependencyGraph(nodes) + print("BUILT GRAPH, HERE IS RESULT:") + g.print_graph() + # 2. define set to keep track of what we already built + projects_built: Set[str] = set() + # 3. perform a breadth-first search to build proper ordering + return output class TestRouteBetweenNodes(unittest.TestCase): def test_route_between_nodes(self): @@ -166,6 +214,21 @@ def test_route_between_nodes(self): # There is a route from node 2 to node 3 self.assertTrue(route_between_nodes(n2, n3)) + +class TestBuildOrder(unittest.TestCase): + def test_build_order_ctci_example(self) -> None: + projects = ['a', 'b', 'c', 'd', 'e', 'f'] + dependencies = [ + ('a', 'd'), + ('f', 'b'), + ('b', 'd'), + ('f', 'a'), + ('d', 'c') + ] + result = build_order(projects, dependencies) + self.assertEqual(result, ['f', 'e', 'a', 'b', 'd', 'c']) + + class TestMyGraphSearch(unittest.TestCase): def test_basic_graph_creation(self): From 351361d7773ea72cb1a20044692b75df9abe3ef8 Mon Sep 17 00:00:00 2001 From: miguelhx Date: Sat, 30 Oct 2021 14:33:14 -0700 Subject: [PATCH 4/6] Finish implementation of 4.7, update test case --- Python/chapter04/p07_build_order/miguelHx.py | 54 +++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py index fbcfb6b6..8bb2e41c 100644 --- a/Python/chapter04/p07_build_order/miguelHx.py +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -16,7 +16,7 @@ from collections import deque from dataclasses import dataclass -from typing import List, Deque, Set, Tuple +from typing import List, Deque, Set, Tuple, Dict @dataclass @@ -173,7 +173,7 @@ def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> Lis # 0. define output output: List[str] = [] # 1. build a dependency graph - project_to_node_map = {} + project_to_node_map: Dict[str, DependencyNode] = {} for p in projects: project_to_node_map[p] = DependencyNode(p, []) for d in dependencies: @@ -183,13 +183,50 @@ def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> Lis nodes = [] for node in project_to_node_map.values(): nodes.append(node) - g = DependencyGraph(nodes) - print("BUILT GRAPH, HERE IS RESULT:") - g.print_graph() + # g = DependencyGraph(nodes) + # print("BUILT GRAPH, HERE IS RESULT:") + # g.print_graph() # 2. define set to keep track of what we already built projects_built: Set[str] = set() - # 3. perform a breadth-first search to build proper ordering + # 3. for each project node, perform a depth-first search + for project_node in project_to_node_map.values(): + # 4. perform a depth first search + visited = set() + stack = [] + stack.append(project_node) + not_built = [] + while stack: + node = stack.pop() + if node.id not in visited: + visited.add(node.id) + # get adjacent vertices of popped node. + # if it has not been visited, push it to stack + for n in node.children: + if n.id not in visited and n.id not in projects_built: + stack.append(n) + # check if all children are built. + all_adjacent_built = True + for n in node.children: + if n.id not in projects_built: + all_adjacent_built = False + # if all adjacent nodes are built, + # then this node can be safely built. + if all_adjacent_built and node.id not in projects_built: + projects_built.add(node.id) + output.append(node.id) + else: + not_built.append(node) + # after traversing, we may have built all children. + # check nodes that haven't been built from this traversal. + for node in not_built: + all_adjacent_built = True + for n in node.children: + if n.id not in projects_built: + all_adjacent_built = False + if all_adjacent_built and node.id not in projects_built: + projects_built.add(node.id) + output.append(node.id) return output class TestRouteBetweenNodes(unittest.TestCase): @@ -226,7 +263,10 @@ def test_build_order_ctci_example(self) -> None: ('d', 'c') ] result = build_order(projects, dependencies) - self.assertEqual(result, ['f', 'e', 'a', 'b', 'd', 'c']) + # this is the textbook answer, but it is possible to get a + # different valid build order (depends on algorithm) + # self.assertEqual(result, ['f', 'e', 'a', 'b', 'd', 'c']) + self.assertEqual(result, ['f', 'a', 'b', 'd', 'c', 'e']) class TestMyGraphSearch(unittest.TestCase): From dad955ca39d3e79707dc415f99818d14b66ae031 Mon Sep 17 00:00:00 2001 From: miguelhx Date: Sat, 30 Oct 2021 14:36:15 -0700 Subject: [PATCH 5/6] Remove unused code --- Python/chapter04/p07_build_order/miguelHx.py | 125 +------------------ 1 file changed, 3 insertions(+), 122 deletions(-) diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py index 8bb2e41c..10f14ca9 100644 --- a/Python/chapter04/p07_build_order/miguelHx.py +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -14,18 +14,8 @@ """ import unittest -from collections import deque from dataclasses import dataclass -from typing import List, Deque, Set, Tuple, Dict - - -@dataclass -class Graph: - nodes: 'List[Node]' - - def print_graph(self): - for node in self.nodes: - node.print_children() +from typing import List, Set, Tuple, Dict @dataclass @@ -46,79 +36,6 @@ def print_children(self): def __str__(self): return f'Node ({self.id}), children: {self.children_as_str()}' -def bfs_search_exhaustive(root: Node) -> List[int]: - """Simple BFS. - takes in a root, returns a list - of ids of the sequence of visited - nodes. Goes through entire graph. - Args: - root (Node): starting node - Returns: - List[int]: List[int]: list of node IDs (i.e. [0, 1, 4]) - """ - visited_list: List[int] = [root.id] - visited: Set[int] = set([root.id]) - queue: Deque[Node] = deque([root]) - while queue: - node = queue.popleft() - # print(f'Visiting node ({node.id})') - for n in node.children: - if n.id not in visited: - queue.append(n) - visited_list.append(n.id) - visited.add(n.id) - return visited_list - - -def bfs_search_for_dest(root: Node, dest: Node) -> List[int]: - """Simple BFS. - takes in a root, returns a list - of ids of the sequence of visited - nodes. Stops at destination node - Args: - root (Node): starting node - Returns: - List[int]: List[int]: list of node IDs (i.e. [0, 1, 4]) - """ - visited_list: List[int] = [root.id] - visited: Set[int] = set([root.id]) - queue: Deque[Node] = deque([root]) - while queue: - node = queue.popleft() - # print(f'Visiting node ({node.id})') - for n in node.children: - if n.id not in visited: - queue.append(n) - visited_list.append(n.id) - visited.add(n.id) - if n.id == dest.id: - # done searching - return visited_list - return visited_list - -def route_between_nodes(src: Node, dest: Node) -> bool: - """This function will return true if a path - is found between two nodes, false otherwise. - The idea is to perform a breadth first search - from src to dest. After obtaining a list of - nodes visited, we simply check to see if destination - node id is in there. - Runtime Complexity: - O(V + E) where V represents the number of - nodes in the graph and E represents the number - of edges in this graph. - Space Complexity: - O(V) where V represents the number of existing nodes - in the graph. - Args: - src (Node): from node - dest (Node): destination node - Returns: - bool: whether a path between src and dest exists - """ - ids_visited: List[int] = bfs_search_for_dest(src, dest) - return dest.id in ids_visited - @dataclass class DependencyNode: @@ -229,28 +146,6 @@ def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> Lis output.append(node.id) return output -class TestRouteBetweenNodes(unittest.TestCase): - def test_route_between_nodes(self): - n0 = Node(0, []) - n1 = Node(1, []) - n2 = Node(2, []) - n3 = Node(3, []) - n4 = Node(4, []) - n5 = Node(5, []) - n0.add_child(n1, n4, n5) - n1.add_child(n3, n4) - n2.add_child(n1) - n3.add_child(n2, n4) - # must remember to reset node visited properties - # before each fresh run - g = Graph([n0, n1, n2, n3, n4, n5]) - # There is a route from node 0 to node 2 - self.assertTrue(route_between_nodes(n0, n2)) - # No route between node 1 and node 0 - self.assertFalse(route_between_nodes(n1, n0)) - # There is a route from node 2 to node 3 - self.assertTrue(route_between_nodes(n2, n3)) - class TestBuildOrder(unittest.TestCase): def test_build_order_ctci_example(self) -> None: @@ -269,7 +164,7 @@ def test_build_order_ctci_example(self) -> None: self.assertEqual(result, ['f', 'a', 'b', 'd', 'c', 'e']) -class TestMyGraphSearch(unittest.TestCase): +class TestMyDependencyGraphSearch(unittest.TestCase): def test_basic_graph_creation(self): n0 = Node(0, []) @@ -287,23 +182,9 @@ def test_basic_graph_creation(self): n5.add_child(n4) n6.add_child(n5) nodes = [n0, n1, n2, n3, n4, n5, n6] - g = Graph(nodes) + g = DependencyGraph(nodes) # g.print_graph() - def test_basic_breadth_first_search_exhaustive(self): - n0 = Node(0, []) - n1 = Node(1, []) - n2 = Node(2, []) - n3 = Node(3, []) - n4 = Node(4, []) - n5 = Node(5, []) - n0.add_child(n1, n4, n5) - n1.add_child(n3, n4) - n2.add_child(n1) - n3.add_child(n2, n4) - result: List[int] = bfs_search_exhaustive(n0) - self.assertEqual(result, [0, 1, 4, 5, 3, 2]) - if __name__ == '__main__': unittest.main() From c7e87588b4683cd82346d10929947d1ee12a9073 Mon Sep 17 00:00:00 2001 From: miguelhx Date: Sat, 30 Oct 2021 14:37:05 -0700 Subject: [PATCH 6/6] Add comment --- Python/chapter04/p07_build_order/miguelHx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/chapter04/p07_build_order/miguelHx.py b/Python/chapter04/p07_build_order/miguelHx.py index 10f14ca9..a8225e00 100644 --- a/Python/chapter04/p07_build_order/miguelHx.py +++ b/Python/chapter04/p07_build_order/miguelHx.py @@ -129,6 +129,8 @@ def build_order(projects: List[str], dependencies: List[Tuple[str, str]]) -> Lis all_adjacent_built = False # if all adjacent nodes are built, # then this node can be safely built. + # otherwise, add to not_built list to check + # if children are built after traversal if all_adjacent_built and node.id not in projects_built: projects_built.add(node.id) output.append(node.id)