biconnected.py 12 KB

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