"""CSC111 Winter 2023 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) 2023 Mario Badr and David Liu. """ from __future__ import annotations from typing import Any class Graph: """A graph. Representation Invariants: - all(item == self._vertices[item].item for item in self._vertices) """ # 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 appear 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 appear 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, 4): ... 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 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. """ if (item not in self._vertices) or (len(self._vertices[item].neighbours) < 2): return False for u in self._vertices[item].neighbours: for w in self._vertices[item].neighbours: if u != w and u.check_connected(w.item, {self._vertices[item]}): 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. """ connected_added = {self.item} visited.add(self) for neighbour in self.neighbours: if neighbour not in visited: connected_added = connected_added.union(neighbour.get_connected_component(visited)) return connected_added if __name__ == '__main__': import doctest doctest.testmod(verbose=True) # When you are ready to check your work with python_ta, uncomment the following lines. # (In PyCharm, select the lines below and press Ctrl/Cmd + / to toggle comments.) # You can use "Run file in Python Console" to run PythonTA, # and then also test your methods manually in the console. import python_ta python_ta.check_all(config={ 'max-line-length': 120, 'max-nested-blocks': 4 })