예제 #1
0
파일: tree.py 프로젝트: cpeisert/vertizee
    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
예제 #2
0
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)
    )
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
    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)))
예제 #6
0
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]
예제 #7
0
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
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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
예제 #11
0
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"
                )