255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
"""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
|
|
})
|