"""CSC111 Winter 2022 Prep 8: Programming Exercises Instructions (READ THIS FIRST!) =============================== This module contains the graph implementation we studied in lecture, with a few additional methods for you to implement on this exercise. We have marked each place you need to write code with the word "TODO". As you complete your work in this file, delete each TODO comment. You may add additional doctests, but they will not be graded. You should test your work carefully before submitting it! Copyright and Usage Information =============================== This file is provided solely for the personal and private use of students taking CSC111 at the University of Toronto St. George campus. All forms of distribution of this code, whether as given or with any changes, are expressly prohibited. For more information on copyright for CSC111 materials, please consult our Course Syllabus. This file is Copyright (c) 2022 Mario Badr and David Liu. """ from __future__ import annotations from typing import Any class Graph: """A graph. """ # Private Instance Attributes: # - _vertices: # A collection of the vertices contained in this graph. # Maps item to _Vertex object. _vertices: dict[Any, _Vertex] def __init__(self) -> None: """Initialize an empty graph (no vertices or edges).""" self._vertices = {} def add_vertex(self, item: Any) -> None: """Add a vertex with the given item to this graph. The new vertex is not adjacent to any other vertices. """ self._vertices[item] = _Vertex(item, set()) def add_edge(self, item1: Any, item2: Any) -> None: """Add an edge between the two vertices with the given items in this graph. Raise a ValueError if item1 or item2 do not appear as vertices in this graph. Preconditions: - item1 != item2 """ if item1 in self._vertices and item2 in self._vertices: v1 = self._vertices[item1] v2 = self._vertices[item2] # Add the new edge v1.neighbours.add(v2) v2.neighbours.add(v1) else: # We didn't find an existing vertex for both items. raise ValueError def connected(self, item1: Any, item2: Any) -> bool: """Return whether item1 and item2 are connected vertices in this graph. Return False if item1 or item2 do not appear as vertices in this graph. >>> g = Graph() >>> g.add_vertex(1) >>> g.add_vertex(2) >>> g.add_vertex(3) >>> g.add_vertex(4) >>> g.add_edge(1, 2) >>> g.add_edge(2, 3) >>> g.connected(1, 3) True >>> g.connected(1, 4) False """ if item1 in self._vertices and item2 in self._vertices: v1 = self._vertices[item1] return v1.check_connected(item2, set()) # Pass in an empty "visited" set else: return False def get_connected_component(self, item: Any) -> set: """Return a set of all ITEMS connected to the given item in this graph. Raise a ValueError if item does not appears as a vertex in this graph. >>> g = Graph() >>> for i in range(0, 5): ... g.add_vertex(i) >>> g.add_edge(0, 1) >>> g.add_edge(1, 2) >>> g.add_edge(1, 3) >>> g.add_edge(2, 3) >>> g.get_connected_component(0) == {0, 1, 2, 3} True Note: we've implemented this method for you, and you should not change it. Instead, your task is to implement _Vertex.get_connected_component below. """ if item not in self._vertices: raise ValueError else: return self._vertices[item].get_connected_component(set()) def in_cycle(self, item: Any) -> bool: """Return whether the given item is in a cycle in this graph. Return False if item does not appears as a vertex in this graph. KEY OBSERVATION. A vertex v is in a cycle if and only if: v has two distinct neighbours u and w that are connected to each other by a path that doesn't use v. >>> g = Graph() >>> for i in range(0, 5): ... g.add_vertex(i) >>> g.add_edge(0, 1) >>> g.add_edge(1, 2) >>> g.add_edge(1, 3) >>> g.add_edge(2, 3) >>> g.in_cycle(1) True >>> g.in_cycle(0) False >>> g.add_edge(4, 0) >>> g.in_cycle(0) False Implementation notes: 1. This method should call _Vertex.check_connected (following the above description). 2. Don't try to make this method recursive, or copy and paste the implementation of _Vertex.check_connected! That's not necessary here. """ # Does not exist if item not in self._vertices: return False v = self._vertices[item] # Combinations for u in v.neighbours: for w in v.neighbours: # Distinct combinations if u == w or u == v or w == v: continue if u.check_connected(w.item, {v}): return True return False class _Vertex: """A vertex in a graph. Instance Attributes: - item: The data stored in this vertex. - neighbours: The vertices that are adjacent to this vertex. Representation Invariants: - self not in self.neighbours - all(self in u.neighbours for u in self.neighbours) """ item: Any neighbours: set[_Vertex] def __init__(self, item: Any, neighbours: set[_Vertex]) -> None: """Initialize a new vertex with the given item and neighbours.""" self.item = item self.neighbours = neighbours def check_connected(self, target_item: Any, visited: set[_Vertex]) -> bool: """Return whether this vertex is connected to a vertex corresponding to the target_item, WITHOUT using any of the vertices in visited. Preconditions: - self not in visited """ if self.item == target_item: # Our base case: the target_item is the current vertex return True else: visited.add(self) # Add self to the set of visited vertices for u in self.neighbours: if u not in visited: # Only recurse on vertices that haven't been visited if u.check_connected(target_item, visited): return True return False def get_connected_component(self, visited: set[_Vertex]) -> set: """Return a set of all ITEMS connected to self by a path that does not use any vertices in visited. The items of the vertices in visited CANNOT appear in the returned set. Preconditions: - self not in visited Implementation notes: 1. This can be implemented in a similar way to _Vertex.check_connected. 2. This method must be recursive, and will have an implicit base case: when all vertices in self.neighbours are already in visited. 3. Use a loop accumulator to store a set of the vertices connected to self. >>> g = Graph() >>> for i in range(0, 7): ... g.add_vertex(i) >>> g.add_edge(0, 1) >>> g.add_edge(1, 2) >>> g.add_edge(1, 3) >>> g.add_edge(2, 3) >>> g.get_connected_component(1) == {0, 1, 2, 3} True >>> g.add_edge(4, 0) >>> g.get_connected_component(0) == {0, 1, 2, 3, 4} True >>> g.get_connected_component(5) {5} >>> g._vertices[5].get_connected_component({g._vertices[5]}) set() >>> g._vertices[6].get_connected_component(set()) {6} """ if self in visited: return set() visited.add(self) nums = {self.item} for u in self.neighbours: nums = nums.union(u.get_connected_component(visited)) return nums if __name__ == '__main__': # Note: we are NOT using python_ta.contracts for this prep. # (Feel free to ask why in office hours/Campuswire.) import doctest doctest.testmod() import python_ta python_ta.check_all(config={ 'max-line-length': 100, 'disable': ['E1136'], 'max-nested-blocks': 4 })