3 # © Copyright 2021-2022, Scott Gasch
5 """A simple graph class that can be optionally directed and weighted and
6 some operations on it."""
9 from typing import Dict, Generator, List, Optional, Set
11 from pyutils.types.simple import Numeric
15 def __init__(self, directed: bool = False):
16 """Constructs a new Graph object.
19 directed: are we modeling a directed graph? See ::meth
23 self.directed = directed
24 self.graph: Dict[str, Dict[str, Numeric]] = {}
26 def add_vertex(self, vertex_id: str) -> bool:
27 """Adds a new vertex to the graph.
30 vertex_id: the unique identifier of the new vertex.
33 True unless vertex_id is already in the graph.
42 >>> len(g.get_vertices())
46 if vertex_id not in self.graph:
47 self.graph[vertex_id] = {}
51 def add_edge(self, src: str, dest: str, weight: Numeric = 1) -> None:
52 """Adds a new (optionally weighted) edge between src and dest
53 vertexes. If the graph is not directed (see c'tor) this also
54 adds a reciprocal edge with the same weight back from dest to
59 If either or both of src and dest are not already added to the
60 graph, they are implicitly added by adding this edge.
63 src: the source vertex id
64 dest: the destination vertex id
65 weight: optionally, the weight of the edge(s) added
68 >>> g.add_edge('a', 'b')
69 >>> g.add_edge('b', 'c', weight=2)
70 >>> len(g.get_vertices())
73 {'a': {'b': 1}, 'b': {'a': 1, 'c': 2}, 'c': {'b': 2}}
78 self.graph[src][dest] = weight
80 self.graph[dest][src] = weight
82 def get_vertices(self) -> List[str]:
85 a list of the vertex ids in the graph.
90 >>> g.add_edge('b', 'c')
94 return list(self.graph.keys())
96 def get_edges(self) -> Dict[str, Dict[str, Numeric]]:
99 A dict whose keys are source vertexes and values
100 are dicts of destination vertexes with values describing the
101 weight of the edge from source to destination.
103 >>> g = Graph(directed=True)
104 >>> g.add_edge('a', 'b')
105 >>> g.add_edge('b', 'c', weight=2)
106 >>> len(g.get_vertices())
109 {'a': {'b': 1}, 'b': {'c': 2}, 'c': {}}
113 def _dfs(self, vertex: str, visited: Set[str]):
116 for neighbor in self.graph[vertex]:
117 if neighbor not in visited:
118 yield from self._dfs(neighbor, visited)
121 self, starting_vertex: str, target: Optional[str] = None
122 ) -> Generator[str, None, None]:
123 """Performs a depth first traversal of the graph.
126 starting_vertex: The DFS starting point.
127 target: The vertex that, if found, indicates to halt.
130 An ordered sequence of vertex ids visited by the traversal.
135 C ------ D ------ E ------ F -O
141 >>> g.add_edge('A', 'B')
142 >>> g.add_edge('A', 'C')
143 >>> g.add_edge('B', 'D')
144 >>> g.add_edge('C', 'D')
145 >>> g.add_edge('D', 'E')
146 >>> g.add_edge('E', 'F')
147 >>> g.add_edge('E', 'G')
148 >>> g.add_edge('F', 'F')
149 >>> for node in g.dfs('A'):
159 >>> for node in g.dfs('F', 'B'):
166 visited: Set[str] = set()
167 for node in self._dfs(starting_vertex, visited):
173 self, starting_vertex: str, target: Optional[str] = None
174 ) -> Generator[str, None, None]:
175 """Performs a breadth first traversal of the graph.
178 starting_vertex: The BFS starting point.
179 target: The vertex that, if found, we should halt the search.
182 An ordered sequence of vertex ids visited by the traversal.
187 C ------ D ------ E ------ F -O
193 >>> g.add_edge('A', 'B')
194 >>> g.add_edge('A', 'C')
195 >>> g.add_edge('B', 'D')
196 >>> g.add_edge('C', 'D')
197 >>> g.add_edge('D', 'E')
198 >>> g.add_edge('E', 'F')
199 >>> g.add_edge('E', 'G')
200 >>> g.add_edge('F', 'F')
201 >>> for node in g.bfs('A'):
211 >>> for node in g.bfs('F', 'G'):
221 todo.append(starting_vertex)
222 visited.add(starting_vertex)
230 neighbors = self.graph[vertex]
231 for neighbor in neighbors:
232 if neighbor not in visited:
233 todo.append(neighbor)
234 visited.add(neighbor)
237 if __name__ == "__main__":