def add_edge(self, edge: E) -> E: """Adds a new edge to the tree. Exactly one of the edge's vertices must already be in the tree. If neither of the edge's endpoints are in the tree, then the edge is unreachable from the existing tree, and by definition, trees must be connected. If both of the edge's endpoints are in the tree, it forms a cycle, and by definition, trees are :term:`acyclic`. Args: edge: The edge to add. Raises: Unfeasible: An ``Unfeasible`` exception is raised if the edge is not already in the tree and either both of its endpoints are in the tree (which would create a cycle) or neither of its endpoints are in the tree (which would make the edge unreachable). """ if edge.label in self._edges: return edge if edge.vertex1.label not in self._vertices and edge.vertex2.label not in self._vertices: raise exception.Unfeasible( f"neither of the edge endpoints {edge} were found in the " "tree; exactly one of the endpoints must already be in the tree" ) if edge.vertex1.label in self._vertices and edge.vertex2.label in self._vertices: raise exception.Unfeasible( f"both of the edge endpoints {edge} were found in the " "tree, which would create a cycle; trees are acyclic") self._edges[edge.label] = cast(E_co, edge) self._vertices[edge.vertex1.label] = edge.vertex1 self._vertices[edge.vertex2.label] = edge.vertex2 return edge
def strongly_connected_components( graph: GraphBase[V_co, E_co] ) -> Iterator["Component[V_co, E_co]"]: """Returns an iterator over the :term:`strongly-connected <strongly connected>` components of the :term:`digraph`. Note: For :term:`directed graphs <digraph>`, this function uses Kosaraju's algorithm to find the strongly connected components (SCC), with the caveat that the SCCs are returned in reverse :term:`topological order <topological ordering>`. This ordering refers to :term:`topologically sorting <topological sorting>` the :term:`condensation graph <condensation>`. Args: graph: The graph to analyze. Yields: Component: An iterator of :class:`Component` objects. Note: This implementation of Kosaraju's algorithm is based on the treatment in Roughgarden. :cite:`2018:roughgarden` """ if len(graph) == 0: raise exception.Unfeasible("components are undefined for an empty graph") if not graph.is_directed(): raise exception.GraphTypeNotSupported("graph must be directed") postorder = list(dfs_module.dfs_postorder_traversal(graph, reverse_graph=True)) return _plain_depth_first_search( graph, adjacency_function=_get_adjacent_to_child, vertices=reversed(postorder) )
def connected_components(graph: "GraphBase[V_co, E_co]") -> Iterator["Component[V_co, E_co]"]: """Returns an iterator over the :term:`connected components <connected component>`; if the :term:`graph` is directed, then the components are the :term:`strongly-connected <strongly connected>` components of the graph. Note: For :term:`directed graphs <digraph>`, this function uses Kosaraju's algorithm to find the strongly connected components (SCC), with the caveat that the SCCs are returned in reverse :term:`topological order <topological ordering>`. This ordering refers to :term:`topologically sorting <topological sorting>` the :term:`condensation graph <condensation>`. Args: graph (G): The graph to analyze. Yields: Component: An iterator of :class:`Component` objects. Note: This implementation of Kosaraju's algorithm is based on the treatment in Roughgarden. :cite:`2018:roughgarden` """ if len(graph) == 0: raise exception.Unfeasible("components are undefined for an empty graph") if graph.is_directed(): return strongly_connected_components(graph) return _plain_depth_first_search(graph, adjacency_function=_get_adjacent_to_child)
def weakly_connected_components(graph: GraphBase[V_co, E_co]) -> Iterator["Component[V_co, E_co]"]: """Returns an iterator over the :term:`weakly-connected <weakly connected>` components of the graph. Args: graph (G): The graph to analyze. Yields: Component: An iterator of :class:`Component` objects. """ if len(graph) == 0: raise exception.Unfeasible("components are undefined for an empty graph") if not graph.is_directed(): raise exception.GraphTypeNotSupported( "weakly-connected components are only defined for directed graphs" ) return _plain_depth_first_search(graph, adjacency_function=_get_adjacent_to_child_undirected)
def vertices_topological_order(self) -> "ListView[V_co]": """Returns a :class:`ListView <vertizee.classes.collection_views.ListView>` of the vertices in a :term:`topological ordering`. Note: The topological ordering is the reverse of the depth-first search postordering. The reverse of the postordering is not the same as the preordering. Raises: Unfeasible: Raises ``Unfeasible`` if the conditions required to determine a topological ordering are not met. See :meth:`has_topological_ordering`. """ if not self.has_topological_ordering(): bfs_error = "breadth-first search used" if not self._depth_first_search else "" dir_error = "graph not directed" if not self._graph.is_directed( ) else "" cycle_error = "graph has cycle" if not self._is_acyclic else "" errors = [bfs_error, dir_error, cycle_error] error_msg = "; ".join(e for e in errors if e) raise exception.Unfeasible( "a topological ordering is only valid for a depth-first " f"search on a directed, acyclic graph; error: {error_msg}") return ListView(list(reversed(self._vertices_postorder)))
def bfs_labeled_edge_traversal( graph: GraphBase[V_co, E_co], source: Optional[VertexType] = None, depth_limit: Optional[float] = None, reverse_graph: bool = False, ) -> Iterator[Tuple[V_co, V_co, str, str, float]]: """Iterates over the labeled :term:`edges <edge>` of a breadth-first search traversal. Running time: :math:`O(m + n)` Note: If ``source`` is specified, then the traversal only includes the graph :term:`component <connected component>` containing the ``source`` vertex. For :term:`directed graphs <digraph>`, setting ``reverse_graph`` to True will generate vertices as if the graph were :term:`reversed <reverse>`. Args: graph: The graph to search. source: The source vertex from which to discover reachable vertices. depth_limit: Optional; The depth limit of the search. Defaults to None (no limit). reverse_graph: Optional; For directed graphs, setting to True will yield a traversal as if the graph were reversed (i.e. the :term:`reverse graph <reverse>`). Defaults to False. Yields: Tuple[Vertex, Vertex, str, str, int]: An iterator over tuples of the form ``(parent, child, label, search_direction, depth)`` where ``(parent, child)`` is the edge being explored in the breadth-first search. The ``label`` is one of the strings: 1. "tree_root" - :math:`(u, u)`, where :math:`u` is the root vertex of a BFS tree. 2. "tree_edge" - edge :math:`(u, v)` is a tree edge if :math:`v` was first discovered by exploring edge :math:`(u, v)`. 3. "back_edge" - back edge :math:`(u, v)` connects vertex :math:`u` to ancestor :math:`v` in a breadth-first tree. Per *Introduction to Algorithms*, self loops are considered back edges. :cite:`2009:clrs` 4. "forward_edge" - non-tree edges :math:`(u, v)` connecting a vertex :math:`u` to a descendant :math:`v` in a breadth-first tree. 5. "cross_edge" - All other edges, which may go between vertices in the same breadth-first tree as long as one vertex is not an ancestor of the other, or they go between vertices in different breadth-first trees. The ``search_direction`` is the direction of traversal and is one of the strings: 1. "preorder" - the traversal discovered new vertex `child` in the BFS. 2. "postorder" - the traversal finished visiting vertex `child` in the BFS. 3. "already_discovered" - the traversal found a non-tree edge connecting to a vertex that was already discovered. The ``depth`` is the count of edges between ``child`` and the root vertex in its breadth-first search tree. If the edge :math:`(parent, child)` is not a tree edge (or the tree root), the ``depth`` defaults to infinity. Example: The labels reveal the complete transcript of the breadth-first search algorithm. >>> from pprint import pprint >>> import vertizee as vz >>> from vertizee.algorithms import search >>> g = vz.DiGraph([(0, 1), (1, 2), (2, 1)]) >>> pprint(list(search.bfs_labeled_edge_traversal(g, source=0))) [(0, 0, 'tree_root', 'preorder', 0), (0, 1, 'tree_edge', 'preorder', 1), (0, 0, 'tree_root', 'postorder', 0), (1, 2, 'tree_edge', 'preorder', 2), (0, 1, 'tree_edge', 'postorder', 1), (2, 1, 'back_edge', 'already_discovered', inf), (1, 2, 'tree_edge', 'postorder', 2)] See Also: * :class:`Direction <vertizee.algorithms.algo_utils.search_utils.Direction>` * :class:`Label <vertizee.algorithms.algo_utils.search_utils.Label>` * :class:`SearchResults <vertizee.algorithms.algo_utils.search_utils.SearchResults>` Note: This function uses ideas from the NetworkX function: `networkx.algorithms.traversal.breadth_first_search.generic_bfs_edges <https://github.com/networkx/networkx/blob/master/networkx/algorithms/traversal/breadth_first_search.py>`_ :cite:`2008:hss` The NetworkX function was in turn adapted from David Eppstein's breadth-first search function in `PADS`. :cite:`2015:eppstein` The edge labeling of this function is based on the treatment in *Introduction to Algorithms*. :cite:`2009:clrs` The feature to allow depth limits is based on Korf. :cite:`1985:korf` """ if len(graph) == 0: raise exception.Unfeasible("search is undefined for an empty graph") classified_edges: Set[str] = set() """The set of edges that have been classified so far by the breadth-first search into one of: tree edge, back edge, cross edge, or forward edge.""" predecessor: Dict[V_co, Optional[V_co]] = collections.defaultdict(lambda: None) """The predecessor is the parent vertex in the BFS tree. Root vertices have predecessor None. In addition, if a source vertex is specified, unreachable vertices also have predecessor None. """ search_trees: UnionFind[V_co] = UnionFind() """UnionFind data structure, where each disjoint set contains the vertices from a breadth-first search tree.""" seen: Set[V_co] = set() """A set of the vertices discovered so far during a breadth-first search.""" vertex_depth: Dict[V_co, float] = collections.defaultdict(lambda: INFINITY) vertices: Union[ValuesView[V_co], Set[V_co]] if source is None: vertices = graph.vertices() else: vertices = {graph[source]} if depth_limit is None: depth_limit = INFINITY for vertex in vertices: if vertex in seen: continue depth_now = 0 search_trees.make_set(vertex) # New BFS tree root. seen.add(vertex) vertex_depth[vertex] = depth_now children = get_adjacent_to_child(child=vertex, parent=None, reverse_graph=reverse_graph) queue: Deque[VertexSearchState[V_co]] = collections.deque() queue.append(VertexSearchState(vertex, children, depth_now)) yield vertex, vertex, Label.TREE_ROOT, Direction.PREORDER, depth_now # Explore the bread-first search tree rooted at `vertex`. while queue: parent_state: VertexSearchState[V_co] = queue.popleft() parent = parent_state.parent for child in parent_state.children: edge_label = edge_module.create_edge_label( parent, child, graph.is_directed()) if child not in seen: # Discovered new vertex? seen.add(child) if parent_state.depth is not None: depth_now = parent_state.depth + 1 vertex_depth[child] = depth_now predecessor[child] = parent search_trees.make_set(child) search_trees.union(parent, child) classified_edges.add(edge_label) yield parent, child, Label.TREE_EDGE, Direction.PREORDER, depth_now grandchildren = get_adjacent_to_child( child=child, parent=parent, reverse_graph=reverse_graph) if depth_now < (depth_limit - 1): queue.append( VertexSearchState(child, grandchildren, depth_now)) elif edge_label not in classified_edges: classified_edges.add(edge_label) classification = _classify_edge(parent, child, vertex_depth, search_trees) yield parent, child, classification, Direction.ALREADY_DISCOVERED, INFINITY if predecessor[parent]: yield ( cast(V_co, predecessor[parent]), parent, Label.TREE_EDGE, Direction.POSTORDER, vertex_depth[parent], ) else: yield parent, parent, Label.TREE_ROOT, Direction.POSTORDER, vertex_depth[ parent]
def kruskal_optimum_forest( graph: GraphBase[V_co, E_co], minimum: bool = True, weight: str = "Edge__weight") -> Iterator[Tree[V_co, E_co]]: r"""Iterates over the minimum (or maximum) :term:`trees <tree>` comprising an :term:`optimum spanning forest` of an :term:`undirected graph` using Kruskal's algorithm. Running time: :math:`O(m(\log{n}))` where :math:`m = |E|` and :math:`n = |V|` This implementation is based on MST-KRUSKAL. :cite:`2009:clrs` Note: This algorithm is only defined for *undirected* graphs. To find the optimum forest of a directed graph (also called an :term:`optimum branching <branching>`), see :func:`optimum_directed_forest <vertizee.algorithms.spanning.directed.optimum_directed_forest>`. Args: graph: The undirected graph to iterate. minimum: Optional; True to return the minimum spanning tree, or False to return the maximum spanning tree. Defaults to True. weight: Optional; The key to use to retrieve the weight from the edge ``attr`` dictionary. The default value ("Edge__weight") uses the edge property ``weight``. Yields: Iterator[Tree[V, E]]: An iterator over the minimum (or maximum) trees. If only one tree is yielded prior to ``StopIteration``, then it is a spanning tree. See Also: * :func:`kruskal_spanning_tree` * :func:`optimum_forest` * :func:`spanning_tree` * :class:`UnionFind <vertizee.classes.data_structures.union_find.UnionFind>` """ if len(graph) == 0: raise exception.Unfeasible("forests are undefined for empty graphs") if graph.is_directed(): raise exception.GraphTypeNotSupported( "graph must be undirected; for directed graphs see optimum_directed_forest" ) weight_function = get_weight_function(weight, minimum=minimum) sign = 1 if minimum else -1 edge_weight_pairs = [(e, sign * weight_function(e)) for e in graph.edges()] sorted_edges = [ p[0] for p in sorted(edge_weight_pairs, key=lambda pair: pair[1]) ] union_find = UnionFind(*graph.vertices()) vertex_to_tree: Dict[V_co, Tree[V_co, E_co]] = {v: Tree(v) for v in graph.vertices()} for edge in sorted_edges: if not union_find.in_same_set(edge.vertex1, edge.vertex2): union_find.union(edge.vertex1, edge.vertex2) vertex_to_tree[edge.vertex1].add_edge(edge) set_iter = union_find.get_sets() for tree_vertex_set in set_iter: tree = vertex_to_tree[tree_vertex_set.pop()] while tree_vertex_set: tree.merge(vertex_to_tree[tree_vertex_set.pop()]) yield tree
def prim_fibonacci( graph: GraphBase[V_co, E_co], root: Optional["VertexType"] = None, minimum: bool = True, weight: str = "Edge__weight", ) -> Iterator[E_co]: r"""Iterates over a minimum (or maximum) :term:`spanning tree` of a weighted, :term:`undirected graph` using Prim's algorithm implemented using a :term:`Fibonacci heap`. Running time: :math:`O(m + n(\log{n}))` where :math:`m = |E|` and :math:`n = |V|` Note: If the graph does not contain a spanning tree, for example, if the graph is disconnected, no error or warning will be raised. If the total number of edges yielded equals :math:`|V| - 1`, then there is a spanning tree, otherwise see :func:`optimum_forest`. Note: This algorithm is only defined for *undirected* graphs. To find the spanning tree of a directed graph, see :func:`spanning_arborescence <vertizee.algorithms.spanning.directed.spanning_arborescence>`. Note: The :term:`Fibonacci-heap <Fibonacci heap>` based implementation of Prim's algorithm is faster than the default :term:`binary-heap <heap>` implementation, since the DECREASE-KEY operation, i.e. :meth:`PriorityQueue.add_or_update() <vertizee.classes.data_structures.priority_queue.PriorityQueue.add_or_update>`, requires :math:`O(\log{n})` time for binary heaps and only :math:`O(1)` amortized time for Fibonacci heaps. Note: This implementation is based on MST-PRIM. :cite:`2009:clrs` Args: graph: The undirected graph to iterate. root: Optional; The root vertex of the spanning tree to be grown. If not specified, an arbitrary root vertex is chosen. Defaults to None. minimum: Optional; True to return the minimum spanning tree, or False to return the maximum spanning tree. Defaults to True. weight: Optional; The key to use to retrieve the weight from the edge ``attr`` dictionary. The default value ("Edge__weight") uses the edge property ``weight``. Yields: E_co: Edges from the minimum (or maximum) spanning tree discovered using Prim's algorithm. See Also: * :func:`optimum_forest` * :func:`prim_spanning_tree` * :class:`FibonacciHeap <vertizee.classes.data_structures.fibonacci_heap.FibonacciHeap>` * :func:`spanning_tree` """ if len(graph) == 0: raise exception.Unfeasible( "spanning trees are undefined for empty graphs") if graph.is_directed(): raise exception.GraphTypeNotSupported( "graph must be undirected; for directed graphs see optimum_directed_forest" ) if root is not None: try: root_vertex = graph[root] except KeyError as error: raise exception.VertexNotFound( f"root vertex '{root}' not in graph") from error else: # pylint: disable=stop-iteration-return root_vertex = next(iter(graph.vertices())) weight_function = get_weight_function(weight, minimum=minimum) predecessor: Dict[VertexBase, Optional[VertexBase]] = collections.defaultdict( lambda: None) """A dictionary mapping a vertex to its predecessor. A predecessor is the parent vertex in the spanning tree. Root vertices have predecessor None.""" priority: Dict[VertexBase, float] = collections.defaultdict(lambda: INFINITY) """Dictionary mapping a vertex to its priority. Default priority is INFINITY.""" def prim_priority_function(v: VertexBase) -> float: return priority[v] fib_heap: FibonacciHeap[VertexBase] = FibonacciHeap(prim_priority_function) for v in graph: fib_heap.insert(v) priority[root_vertex] = 0 fib_heap.update_item_with_decreased_priority(root_vertex) vertices_in_tree = set() tree_edge: Optional[E_co] = None sign = 1 if minimum else -1 while fib_heap: u = fib_heap.extract_min() assert u is not None # For mypy static type checker. vertices_in_tree.add(u) if predecessor[u]: parent = predecessor[u] assert parent is not None adj_vertices = u.adj_vertices() - {parent} tree_edge = graph.get_edge(parent, u) else: adj_vertices = u.adj_vertices() for v in adj_vertices: u_v_weight = sign * weight_function(graph.get_edge(u, v)) if v not in vertices_in_tree and u_v_weight < priority[v]: predecessor[v] = u priority[v] = u_v_weight fib_heap.update_item_with_decreased_priority(v) if tree_edge: yield tree_edge
def prim_spanning_tree( graph: GraphBase[V_co, E_co], root: Optional["VertexType"] = None, minimum: bool = True, weight: str = "Edge__weight", ) -> Iterator[E_co]: r"""Iterates over a minimum (or maximum) :term:`spanning tree` of a weighted, :term:`undirected graph` using Prim's algorithm. Running time: :math:`O(m(\log{n}))` where :math:`m = |E|` and :math:`n = |V|` Note: If the graph does not contain a spanning tree, for example, if the graph is disconnected, no error or warning will be raised. If the total number of edges yielded equals :math:`|V| - 1`, then there is a spanning tree, otherwise see :func:`optimum_forest`. Note: This algorithm is only defined for *undirected* graphs. To find the spanning tree of a directed graph, see :func:`spanning_arborescence <vertizee.algorithms.spanning.directed.spanning_arborescence>`. Note: Prim's algorithm (implemented with a binary-heap-based :term:`priority queue`) has the same asymptotic running time as Kruskal's algorithm. However, in practice, Kruskal's algorithm often outperforms Prim's algorithm, since the Vertizee implementation of Kruskal's algorithm uses the highly-efficient :class:`UnionFind <vertizee.classes.data_structures.union_find.UnionFind>` data structure. Note: This implementation is based on MST-PRIM. :cite:`2009:clrs` Args: graph: The undirected graph to iterate. root: Optional; The root vertex of the spanning tree to be grown. If not specified, an arbitrary root vertex is chosen. Defaults to None. minimum: Optional; True to return the minimum spanning tree, or False to return the maximum spanning tree. Defaults to True. weight: Optional; The key to use to retrieve the weight from the edge ``attr`` dictionary. The default value ("Edge__weight") uses the edge property ``weight``. Yields: E_co: Edges from the minimum (or maximum) spanning tree discovered using Prim's algorithm. See Also: * :func:`optimum_forest` * :class:`Priority Queue <vertizee.classes.data_structures.priority_queue.PriorityQueue>` * :func:`spanning_tree` """ if len(graph) == 0: raise exception.Unfeasible( "spanning trees are undefined for empty graphs") if graph.is_directed(): raise exception.GraphTypeNotSupported( "graph must be undirected; for directed graphs see optimum_directed_forest" ) if root is not None: try: root_vertex = graph[root] except KeyError as error: raise exception.VertexNotFound( f"root vertex '{root}' not in graph") from error else: # pylint: disable=stop-iteration-return root_vertex = next(iter(graph.vertices())) weight_function = get_weight_function(weight, minimum=minimum) predecessor: Dict[VertexBase, Optional[VertexBase]] = collections.defaultdict( lambda: None) """A dictionary mapping a vertex to its predecessor. A predecessor is the parent vertex in the spanning tree. Root vertices have predecessor None.""" priority: Dict[VertexBase, float] = collections.defaultdict(lambda: INFINITY) """Dictionary mapping a vertex to its priority. Default priority is INFINITY.""" def prim_priority_function(v: VertexBase) -> float: return priority[v] priority_queue: PriorityQueue[VertexBase] = PriorityQueue( prim_priority_function) for v in graph: priority_queue.add_or_update(v) priority[root_vertex] = 0 priority_queue.add_or_update(root_vertex) vertices_in_tree = set() tree_edge: Optional[E_co] = None sign = 1 if minimum else -1 while priority_queue: u = priority_queue.pop() vertices_in_tree.add(u) if predecessor[u]: parent = predecessor[u] assert parent is not None adj_vertices = u.adj_vertices() - {parent} tree_edge = graph.get_edge(parent, u) else: adj_vertices = u.adj_vertices() for v in adj_vertices: u_v_weight = sign * weight_function(graph.get_edge(u, v)) if v not in vertices_in_tree and u_v_weight < priority[v]: predecessor[v] = u priority[v] = u_v_weight priority_queue.add_or_update(v) if tree_edge: yield tree_edge
def kruskal_spanning_tree(graph: GraphBase[V_co, E_co], minimum: bool = True, weight: str = "Edge__weight") -> Iterator[E_co]: r"""Iterates over a minimum (or maximum) :term:`spanning tree` of a weighted, :term:`undirected graph` using Kruskal's algorithm. Running time: :math:`O(m(\log{n}))` where :math:`m = |E|` and :math:`n = |V|` Note: If the graph does not contain a spanning tree, for example, if the graph is disconnected, no error or warning will be raised. If the total number of edges yielded equals :math:`|V| - 1`, then there is a spanning tree, otherwise see :func:`optimum_forest`. Note: This algorithm is only defined for *undirected* graphs. To find the spanning tree of a directed graph, see :func:`spanning_arborescence <vertizee.algorithms.spanning.directed.spanning_arborescence>`. Note: This implementation is based on MST-KRUSKAL. :cite:`2009:clrs` Args: graph: The undirected graph to iterate. minimum: Optional; True to return the minimum spanning tree, or False to return the maximum spanning tree. Defaults to True. weight: Optional; The key to use to retrieve the weight from the edge ``attr`` dictionary. The default value ("Edge__weight") uses the edge property ``weight``. Yields: E_co: An iterator over the edges of the minimum (or maximum) spanning tree discovered using Kruskal's algorithm. Raises: Unfeasible: If the graph does not contain a spanning tree, an Unfeasible exception is raised. See Also: * :func:`optimum_forest` * :func:`spanning_tree` * :class:`UnionFind <vertizee.classes.data_structures.union_find.UnionFind>` """ if len(graph) == 0: raise exception.Unfeasible( "spanning trees are undefined for empty graphs") if graph.is_directed(): raise exception.GraphTypeNotSupported( "graph must be undirected; for directed graphs see optimum_directed_forest" ) weight_function = get_weight_function(weight, minimum=minimum) sign = 1 if minimum else -1 edge_weight_pairs = [(e, sign * weight_function(e)) for e in graph.edges()] sorted_edges = [ p[0] for p in sorted(edge_weight_pairs, key=lambda pair: pair[1]) ] union_find = UnionFind(*graph.vertices()) for edge in sorted_edges: if not union_find.in_same_set(edge.vertex1, edge.vertex2): union_find.union(edge.vertex1, edge.vertex2) yield edge
def dfs_labeled_edge_traversal( graph: GraphBase[V_co, E_co], source: Optional[VertexType] = None, depth_limit: Optional[int] = None, reverse_graph: bool = False, ) -> Iterator[Tuple[V_co, V_co, str, str]]: """Iterates over the labeled edges of a depth-first search traversal. Running time: :math:`O(m + n)` Note: If ``source`` is specified, then the traversal only includes the graph :term:`component <connected component>` containing the ``source`` vertex. Args: graph: The graph to search. source: Optional; The source vertex from which to begin the search. When ``source`` is specified, only the component reachable from the source is searched. Defaults to None. depth_limit: Optional; The depth limit of the search. Defaults to None (no limit). reverse_graph: Optional; For directed graphs, setting to True will yield a traversal as if the graph were reversed (i.e. the :term:`reverse graph <reverse>`). Defaults to False. Yields: Tuple[Vertex, Vertex, str, str]: An iterator over tuples of the form ``(parent, child, label, search_direction)`` where ``(parent, child)`` is the edge being explored in the depth-first search. The ``child`` vertex is found by iterating over the parent's adjacency list. The ``label`` is one of the strings: 1. "tree_root" - :math:`(u, u)`, where :math:`u` is the root vertex of a DFS tree. 2. "tree_edge" - edge :math:`(u, v)` is a tree edge if :math:`v` was first discovered by exploring edge :math:`(u, v)`. 3. "back_edge" - back edge :math:`(u, v)` connects vertex :math:`u` to ancestor :math:`v` in a depth-first tree. Per *Introduction to Algorithms* :cite:`2009:clrs`, :term:`self loops <loop>` are considered back edges. 4. "forward_edge" - non-tree edges :math:`(u, v)` connecting a vertex :math:`u` to a descendant :math:`v` in a depth-first tree. 5. "cross_edge" - All other edges, which may go between vertices in the same depth-first tree as long as one vertex is not an ancestor of the other, or they go between vertices in different depth-first trees. In an undirected graph, every edge is either a tree edge or a back edge. The ``search_direction`` is the direction of traversal and is one of the strings: 1. "preorder" - the traversal discovered new vertex `child` in the DFS. 2. "postorder" - the traversal finished visiting vertex `child` in the DFS. 3. "already_discovered" - the traversal found a non-tree edge connecting to a vertex that was already discovered. Example: The labels reveal the complete transcript of the depth-first search algorithm. >>> from pprint import pprint >>> import vertizee as vz >>> from vertizee.algorithms import dfs_labeled_edge_traversal >>> g = vz.DiGraph([(0, 1), (1, 2), (2, 1)]) >>> pprint(list(dfs_labeled_edge_traversal(g, source=0))) [(0, 0, 'tree_root', 'preorder'), (0, 1, 'tree_edge', 'preorder'), (1, 2, 'tree_edge', 'preorder'), (2, 1, 'back_edge', 'already_discovered'), (1, 2, 'tree_edge', 'postorder'), (0, 1, 'tree_edge', 'postorder'), (0, 0, 'tree_root', 'postorder')] See Also: * :class:`Direction <vertizee.algorithms.algo_utils.search_utils.Direction>` * :class:`Label <vertizee.algorithms.algo_utils.search_utils.Label>` * :class:`SearchResults <vertizee.algorithms.algo_utils.search_utils.SearchResults>` Note: This function is adapted from the NetworkX function: `networkx.algorithms.traversal.depth_first_search.dfs_labeled_edges <https://github.com/networkx/networkx/blob/master/networkx/algorithms/traversal/depth_first_search.py>`_ :cite:`2008:hss` The NetworkX function was in turn adapted from David Eppstein's depth-first search function in `PADS`. :cite:`2015:eppstein` The edge labeling of this function is based on the treatment in *Introduction to Algorithms*. :cite:`2009:clrs` The feature to allow depth limits is based on Korf. :cite:`1985:korf` """ if len(graph) == 0: raise exception.Unfeasible("search is undefined for an empty graph") classified_edges: Set[str] = set() """The set of edges that have been classified so far by the depth-first search into one of: tree edge, back edge, cross edge, or forward edge.""" vertex_color: VertexDict[str] = VertexDict() """A mapping from vertices to their color (white, gray, black) indicating the status of each vertex in the search process (i.e. undiscovered, in the process of being visited, or visit finished).""" vertex_discovery_order: VertexDict[int] = VertexDict() """A mapping from vertices to the order in which they were discovered by the depth-first search.""" for vertex in graph.vertices(): vertex_color[vertex] = WHITE if source is None: vertices: Union[ValuesView[V_co], Set[Any]] = graph.vertices() else: s: V_co = graph[source] vertices = {s} for vertex in vertices: vertex = cast( V_co, vertex) # Without cast, Pylance assumes type Union[V_co, Any] if vertex_color[vertex] != WHITE: # Already discovered? continue vertex_color[vertex] = GRAY # Mark discovered. vertex_discovery_order[vertex] = len(vertex_discovery_order) children = get_adjacent_to_child(child=vertex, parent=None, reverse_graph=reverse_graph) stack = [VertexSearchState(vertex, children, depth_limit)] yield vertex, vertex, Label.TREE_ROOT, Direction.PREORDER # Explore the depth-first search tree rooted at `vertex`. while stack: parent = stack[-1].parent children = stack[-1].children depth_now = stack[-1].depth try: child = next(children) except StopIteration: stack_frame = stack.pop() child = stack_frame.parent vertex_color[child] = BLACK # Finished visiting child. if stack: parent = stack[-1].parent yield parent, child, Label.TREE_EDGE, Direction.POSTORDER else: yield child, child, Label.TREE_ROOT, Direction.POSTORDER continue edge_label = edge_module.create_edge_label(parent, child, graph.is_directed()) if vertex_color[child] == WHITE: # Discovered new vertex? vertex_color[ child] = GRAY # Mark discovered and in the process of being visited. vertex_discovery_order[child] = len(vertex_discovery_order) classified_edges.add(edge_label) yield parent, child, Label.TREE_EDGE, Direction.PREORDER grandchildren = get_adjacent_to_child( child=child, parent=parent, reverse_graph=reverse_graph) if depth_now is None: stack.append( VertexSearchState(child, grandchildren, depth_now)) elif depth_now > 1: stack.append( VertexSearchState(child, grandchildren, depth_now - 1)) elif vertex_color[ child] == GRAY: # In the process of being visited? if edge_label not in classified_edges: classified_edges.add(edge_label) yield parent, child, Label.BACK_EDGE, Direction.ALREADY_DISCOVERED elif vertex_color[child] == BLACK: # Finished being visited? if edge_label not in classified_edges: classified_edges.add(edge_label) if vertex_discovery_order[parent] < vertex_discovery_order[ child]: yield parent, child, Label.FORWARD_EDGE, Direction.ALREADY_DISCOVERED else: yield parent, child, Label.CROSS_EDGE, Direction.ALREADY_DISCOVERED else: raise exception.AlgorithmError( f"vertex color '{vertex_color[child]}' of vertex '{child}' not recognized" )