chains.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """Functions for finding chains in a graph."""
  2. import networkx as nx
  3. from networkx.utils import not_implemented_for
  4. __all__ = ["chain_decomposition"]
  5. @not_implemented_for("directed")
  6. @not_implemented_for("multigraph")
  7. @nx._dispatchable
  8. def chain_decomposition(G, root=None):
  9. """Returns the chain decomposition of a graph.
  10. The *chain decomposition* of a graph with respect a depth-first
  11. search tree is a set of cycles or paths derived from the set of
  12. fundamental cycles of the tree in the following manner. Consider
  13. each fundamental cycle with respect to the given tree, represented
  14. as a list of edges beginning with the nontree edge oriented away
  15. from the root of the tree. For each fundamental cycle, if it
  16. overlaps with any previous fundamental cycle, just take the initial
  17. non-overlapping segment, which is a path instead of a cycle. Each
  18. cycle or path is called a *chain*. For more information, see [1]_.
  19. Parameters
  20. ----------
  21. G : undirected graph
  22. root : node (optional)
  23. A node in the graph `G`. If specified, only the chain
  24. decomposition for the connected component containing this node
  25. will be returned. This node indicates the root of the depth-first
  26. search tree.
  27. Yields
  28. ------
  29. chain : list
  30. A list of edges representing a chain. There is no guarantee on
  31. the orientation of the edges in each chain (for example, if a
  32. chain includes the edge joining nodes 1 and 2, the chain may
  33. include either (1, 2) or (2, 1)).
  34. Raises
  35. ------
  36. NodeNotFound
  37. If `root` is not in the graph `G`.
  38. Examples
  39. --------
  40. >>> G = nx.Graph([(0, 1), (1, 4), (3, 4), (3, 5), (4, 5)])
  41. >>> list(nx.chain_decomposition(G))
  42. [[(4, 5), (5, 3), (3, 4)]]
  43. Notes
  44. -----
  45. The worst-case running time of this implementation is linear in the
  46. number of nodes and number of edges [1]_.
  47. References
  48. ----------
  49. .. [1] Jens M. Schmidt (2013). "A simple test on 2-vertex-
  50. and 2-edge-connectivity." *Information Processing Letters*,
  51. 113, 241–244. Elsevier. <https://doi.org/10.1016/j.ipl.2013.01.016>
  52. """
  53. def _dfs_cycle_forest(G, root=None):
  54. """Builds a directed graph composed of cycles from the given graph.
  55. `G` is an undirected simple graph. `root` is a node in the graph
  56. from which the depth-first search is started.
  57. This function returns both the depth-first search cycle graph
  58. (as a :class:`~networkx.DiGraph`) and the list of nodes in
  59. depth-first preorder. The depth-first search cycle graph is a
  60. directed graph whose edges are the edges of `G` oriented toward
  61. the root if the edge is a tree edge and away from the root if
  62. the edge is a non-tree edge. If `root` is not specified, this
  63. performs a depth-first search on each connected component of `G`
  64. and returns a directed forest instead.
  65. If `root` is not in the graph, this raises :exc:`KeyError`.
  66. """
  67. # Create a directed graph from the depth-first search tree with
  68. # root node `root` in which tree edges are directed toward the
  69. # root and nontree edges are directed away from the root. For
  70. # each node with an incident nontree edge, this creates a
  71. # directed cycle starting with the nontree edge and returning to
  72. # that node.
  73. #
  74. # The `parent` node attribute stores the parent of each node in
  75. # the DFS tree. The `nontree` edge attribute indicates whether
  76. # the edge is a tree edge or a nontree edge.
  77. #
  78. # We also store the order of the nodes found in the depth-first
  79. # search in the `nodes` list.
  80. H = nx.DiGraph()
  81. nodes = []
  82. for u, v, d in nx.dfs_labeled_edges(G, source=root):
  83. if d == "forward":
  84. # `dfs_labeled_edges()` yields (root, root, 'forward')
  85. # if it is beginning the search on a new connected
  86. # component.
  87. if u == v:
  88. H.add_node(v, parent=None)
  89. nodes.append(v)
  90. else:
  91. H.add_node(v, parent=u)
  92. H.add_edge(v, u, nontree=False)
  93. nodes.append(v)
  94. # `dfs_labeled_edges` considers nontree edges in both
  95. # orientations, so we need to not add the edge if it its
  96. # other orientation has been added.
  97. elif d == "nontree" and v not in H[u]:
  98. H.add_edge(v, u, nontree=True)
  99. else:
  100. # Do nothing on 'reverse' edges; we only care about
  101. # forward and nontree edges.
  102. pass
  103. return H, nodes
  104. def _build_chain(G, u, v, visited):
  105. """Generate the chain starting from the given nontree edge.
  106. `G` is a DFS cycle graph as constructed by
  107. :func:`_dfs_cycle_graph`. The edge (`u`, `v`) is a nontree edge
  108. that begins a chain. `visited` is a set representing the nodes
  109. in `G` that have already been visited.
  110. This function yields the edges in an initial segment of the
  111. fundamental cycle of `G` starting with the nontree edge (`u`,
  112. `v`) that includes all the edges up until the first node that
  113. appears in `visited`. The tree edges are given by the 'parent'
  114. node attribute. The `visited` set is updated to add each node in
  115. an edge yielded by this function.
  116. """
  117. while v not in visited:
  118. yield u, v
  119. visited.add(v)
  120. u, v = v, G.nodes[v]["parent"]
  121. yield u, v
  122. # Check if the root is in the graph G. If not, raise NodeNotFound
  123. if root is not None and root not in G:
  124. raise nx.NodeNotFound(f"Root node {root} is not in graph")
  125. # Create a directed version of H that has the DFS edges directed
  126. # toward the root and the nontree edges directed away from the root
  127. # (in each connected component).
  128. H, nodes = _dfs_cycle_forest(G, root)
  129. # Visit the nodes again in DFS order. For each node, and for each
  130. # nontree edge leaving that node, compute the fundamental cycle for
  131. # that nontree edge starting with that edge. If the fundamental
  132. # cycle overlaps with any visited nodes, just take the prefix of the
  133. # cycle up to the point of visited nodes.
  134. #
  135. # We repeat this process for each connected component (implicitly,
  136. # since `nodes` already has a list of the nodes grouped by connected
  137. # component).
  138. visited = set()
  139. for u in nodes:
  140. visited.add(u)
  141. # For each nontree edge going out of node u...
  142. edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d)
  143. for u, v in edges:
  144. # Create the cycle or cycle prefix starting with the
  145. # nontree edge.
  146. chain = list(_build_chain(H, u, v, visited))
  147. yield chain