biconnected.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. """Biconnected components and articulation points."""
  2. from itertools import chain
  3. from networkx.utils.decorators import not_implemented_for
  4. __all__ = [
  5. "biconnected_components",
  6. "biconnected_component_edges",
  7. "is_biconnected",
  8. "articulation_points",
  9. ]
  10. @not_implemented_for("directed")
  11. def is_biconnected(G):
  12. """Returns True if the graph is biconnected, False otherwise.
  13. A graph is biconnected if, and only if, it cannot be disconnected by
  14. removing only one node (and all edges incident on that node). If
  15. removing a node increases the number of disconnected components
  16. in the graph, that node is called an articulation point, or cut
  17. vertex. A biconnected graph has no articulation points.
  18. Parameters
  19. ----------
  20. G : NetworkX Graph
  21. An undirected graph.
  22. Returns
  23. -------
  24. biconnected : bool
  25. True if the graph is biconnected, False otherwise.
  26. Raises
  27. ------
  28. NetworkXNotImplemented
  29. If the input graph is not undirected.
  30. Examples
  31. --------
  32. >>> G = nx.path_graph(4)
  33. >>> print(nx.is_biconnected(G))
  34. False
  35. >>> G.add_edge(0, 3)
  36. >>> print(nx.is_biconnected(G))
  37. True
  38. See Also
  39. --------
  40. biconnected_components
  41. articulation_points
  42. biconnected_component_edges
  43. is_strongly_connected
  44. is_weakly_connected
  45. is_connected
  46. is_semiconnected
  47. Notes
  48. -----
  49. The algorithm to find articulation points and biconnected
  50. components is implemented using a non-recursive depth-first-search
  51. (DFS) that keeps track of the highest level that back edges reach
  52. in the DFS tree. A node `n` is an articulation point if, and only
  53. if, there exists a subtree rooted at `n` such that there is no
  54. back edge from any successor of `n` that links to a predecessor of
  55. `n` in the DFS tree. By keeping track of all the edges traversed
  56. by the DFS we can obtain the biconnected components because all
  57. edges of a bicomponent will be traversed consecutively between
  58. articulation points.
  59. References
  60. ----------
  61. .. [1] Hopcroft, J.; Tarjan, R. (1973).
  62. "Efficient algorithms for graph manipulation".
  63. Communications of the ACM 16: 372–378. doi:10.1145/362248.362272
  64. """
  65. bccs = biconnected_components(G)
  66. try:
  67. bcc = next(bccs)
  68. except StopIteration:
  69. # No bicomponents (empty graph?)
  70. return False
  71. try:
  72. next(bccs)
  73. except StopIteration:
  74. # Only one bicomponent
  75. return len(bcc) == len(G)
  76. else:
  77. # Multiple bicomponents
  78. return False
  79. @not_implemented_for("directed")
  80. def biconnected_component_edges(G):
  81. """Returns a generator of lists of edges, one list for each biconnected
  82. component of the input graph.
  83. Biconnected components are maximal subgraphs such that the removal of a
  84. node (and all edges incident on that node) will not disconnect the
  85. subgraph. Note that nodes may be part of more than one biconnected
  86. component. Those nodes are articulation points, or cut vertices.
  87. However, each edge belongs to one, and only one, biconnected component.
  88. Notice that by convention a dyad is considered a biconnected component.
  89. Parameters
  90. ----------
  91. G : NetworkX Graph
  92. An undirected graph.
  93. Returns
  94. -------
  95. edges : generator of lists
  96. Generator of lists of edges, one list for each bicomponent.
  97. Raises
  98. ------
  99. NetworkXNotImplemented
  100. If the input graph is not undirected.
  101. Examples
  102. --------
  103. >>> G = nx.barbell_graph(4, 2)
  104. >>> print(nx.is_biconnected(G))
  105. False
  106. >>> bicomponents_edges = list(nx.biconnected_component_edges(G))
  107. >>> len(bicomponents_edges)
  108. 5
  109. >>> G.add_edge(2, 8)
  110. >>> print(nx.is_biconnected(G))
  111. True
  112. >>> bicomponents_edges = list(nx.biconnected_component_edges(G))
  113. >>> len(bicomponents_edges)
  114. 1
  115. See Also
  116. --------
  117. is_biconnected,
  118. biconnected_components,
  119. articulation_points,
  120. Notes
  121. -----
  122. The algorithm to find articulation points and biconnected
  123. components is implemented using a non-recursive depth-first-search
  124. (DFS) that keeps track of the highest level that back edges reach
  125. in the DFS tree. A node `n` is an articulation point if, and only
  126. if, there exists a subtree rooted at `n` such that there is no
  127. back edge from any successor of `n` that links to a predecessor of
  128. `n` in the DFS tree. By keeping track of all the edges traversed
  129. by the DFS we can obtain the biconnected components because all
  130. edges of a bicomponent will be traversed consecutively between
  131. articulation points.
  132. References
  133. ----------
  134. .. [1] Hopcroft, J.; Tarjan, R. (1973).
  135. "Efficient algorithms for graph manipulation".
  136. Communications of the ACM 16: 372–378. doi:10.1145/362248.362272
  137. """
  138. yield from _biconnected_dfs(G, components=True)
  139. @not_implemented_for("directed")
  140. def biconnected_components(G):
  141. """Returns a generator of sets of nodes, one set for each biconnected
  142. component of the graph
  143. Biconnected components are maximal subgraphs such that the removal of a
  144. node (and all edges incident on that node) will not disconnect the
  145. subgraph. Note that nodes may be part of more than one biconnected
  146. component. Those nodes are articulation points, or cut vertices. The
  147. removal of articulation points will increase the number of connected
  148. components of the graph.
  149. Notice that by convention a dyad is considered a biconnected component.
  150. Parameters
  151. ----------
  152. G : NetworkX Graph
  153. An undirected graph.
  154. Returns
  155. -------
  156. nodes : generator
  157. Generator of sets of nodes, one set for each biconnected component.
  158. Raises
  159. ------
  160. NetworkXNotImplemented
  161. If the input graph is not undirected.
  162. Examples
  163. --------
  164. >>> G = nx.lollipop_graph(5, 1)
  165. >>> print(nx.is_biconnected(G))
  166. False
  167. >>> bicomponents = list(nx.biconnected_components(G))
  168. >>> len(bicomponents)
  169. 2
  170. >>> G.add_edge(0, 5)
  171. >>> print(nx.is_biconnected(G))
  172. True
  173. >>> bicomponents = list(nx.biconnected_components(G))
  174. >>> len(bicomponents)
  175. 1
  176. You can generate a sorted list of biconnected components, largest
  177. first, using sort.
  178. >>> G.remove_edge(0, 5)
  179. >>> [len(c) for c in sorted(nx.biconnected_components(G), key=len, reverse=True)]
  180. [5, 2]
  181. If you only want the largest connected component, it's more
  182. efficient to use max instead of sort.
  183. >>> Gc = max(nx.biconnected_components(G), key=len)
  184. To create the components as subgraphs use:
  185. ``(G.subgraph(c).copy() for c in biconnected_components(G))``
  186. See Also
  187. --------
  188. is_biconnected
  189. articulation_points
  190. biconnected_component_edges
  191. k_components : this function is a special case where k=2
  192. bridge_components : similar to this function, but is defined using
  193. 2-edge-connectivity instead of 2-node-connectivity.
  194. Notes
  195. -----
  196. The algorithm to find articulation points and biconnected
  197. components is implemented using a non-recursive depth-first-search
  198. (DFS) that keeps track of the highest level that back edges reach
  199. in the DFS tree. A node `n` is an articulation point if, and only
  200. if, there exists a subtree rooted at `n` such that there is no
  201. back edge from any successor of `n` that links to a predecessor of
  202. `n` in the DFS tree. By keeping track of all the edges traversed
  203. by the DFS we can obtain the biconnected components because all
  204. edges of a bicomponent will be traversed consecutively between
  205. articulation points.
  206. References
  207. ----------
  208. .. [1] Hopcroft, J.; Tarjan, R. (1973).
  209. "Efficient algorithms for graph manipulation".
  210. Communications of the ACM 16: 372–378. doi:10.1145/362248.362272
  211. """
  212. for comp in _biconnected_dfs(G, components=True):
  213. yield set(chain.from_iterable(comp))
  214. @not_implemented_for("directed")
  215. def articulation_points(G):
  216. """Yield the articulation points, or cut vertices, of a graph.
  217. An articulation point or cut vertex is any node whose removal (along with
  218. all its incident edges) increases the number of connected components of
  219. a graph. An undirected connected graph without articulation points is
  220. biconnected. Articulation points belong to more than one biconnected
  221. component of a graph.
  222. Notice that by convention a dyad is considered a biconnected component.
  223. Parameters
  224. ----------
  225. G : NetworkX Graph
  226. An undirected graph.
  227. Yields
  228. ------
  229. node
  230. An articulation point in the graph.
  231. Raises
  232. ------
  233. NetworkXNotImplemented
  234. If the input graph is not undirected.
  235. Examples
  236. --------
  237. >>> G = nx.barbell_graph(4, 2)
  238. >>> print(nx.is_biconnected(G))
  239. False
  240. >>> len(list(nx.articulation_points(G)))
  241. 4
  242. >>> G.add_edge(2, 8)
  243. >>> print(nx.is_biconnected(G))
  244. True
  245. >>> len(list(nx.articulation_points(G)))
  246. 0
  247. See Also
  248. --------
  249. is_biconnected
  250. biconnected_components
  251. biconnected_component_edges
  252. Notes
  253. -----
  254. The algorithm to find articulation points and biconnected
  255. components is implemented using a non-recursive depth-first-search
  256. (DFS) that keeps track of the highest level that back edges reach
  257. in the DFS tree. A node `n` is an articulation point if, and only
  258. if, there exists a subtree rooted at `n` such that there is no
  259. back edge from any successor of `n` that links to a predecessor of
  260. `n` in the DFS tree. By keeping track of all the edges traversed
  261. by the DFS we can obtain the biconnected components because all
  262. edges of a bicomponent will be traversed consecutively between
  263. articulation points.
  264. References
  265. ----------
  266. .. [1] Hopcroft, J.; Tarjan, R. (1973).
  267. "Efficient algorithms for graph manipulation".
  268. Communications of the ACM 16: 372–378. doi:10.1145/362248.362272
  269. """
  270. seen = set()
  271. for articulation in _biconnected_dfs(G, components=False):
  272. if articulation not in seen:
  273. seen.add(articulation)
  274. yield articulation
  275. @not_implemented_for("directed")
  276. def _biconnected_dfs(G, components=True):
  277. # depth-first search algorithm to generate articulation points
  278. # and biconnected components
  279. visited = set()
  280. for start in G:
  281. if start in visited:
  282. continue
  283. discovery = {start: 0} # time of first discovery of node during search
  284. low = {start: 0}
  285. root_children = 0
  286. visited.add(start)
  287. edge_stack = []
  288. stack = [(start, start, iter(G[start]))]
  289. edge_index = {}
  290. while stack:
  291. grandparent, parent, children = stack[-1]
  292. try:
  293. child = next(children)
  294. if grandparent == child:
  295. continue
  296. if child in visited:
  297. if discovery[child] <= discovery[parent]: # back edge
  298. low[parent] = min(low[parent], discovery[child])
  299. if components:
  300. edge_index[parent, child] = len(edge_stack)
  301. edge_stack.append((parent, child))
  302. else:
  303. low[child] = discovery[child] = len(discovery)
  304. visited.add(child)
  305. stack.append((parent, child, iter(G[child])))
  306. if components:
  307. edge_index[parent, child] = len(edge_stack)
  308. edge_stack.append((parent, child))
  309. except StopIteration:
  310. stack.pop()
  311. if len(stack) > 1:
  312. if low[parent] >= discovery[grandparent]:
  313. if components:
  314. ind = edge_index[grandparent, parent]
  315. yield edge_stack[ind:]
  316. del edge_stack[ind:]
  317. else:
  318. yield grandparent
  319. low[grandparent] = min(low[parent], low[grandparent])
  320. elif stack: # length 1 so grandparent is root
  321. root_children += 1
  322. if components:
  323. ind = edge_index[grandparent, parent]
  324. yield edge_stack[ind:]
  325. del edge_stack[ind:]
  326. if not components:
  327. # root node is articulation point if it has more than 1 child
  328. if root_children > 1:
  329. yield start