chains.py 6.8 KB

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