kcutsets.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """
  2. Kanevsky all minimum node k cutsets algorithm.
  3. """
  4. import copy
  5. from collections import defaultdict
  6. from itertools import combinations
  7. from operator import itemgetter
  8. import networkx as nx
  9. from networkx.algorithms.flow import (
  10. build_residual_network,
  11. edmonds_karp,
  12. shortest_augmenting_path,
  13. )
  14. from .utils import build_auxiliary_node_connectivity
  15. default_flow_func = edmonds_karp
  16. __all__ = ["all_node_cuts"]
  17. def all_node_cuts(G, k=None, flow_func=None):
  18. r"""Returns all minimum k cutsets of an undirected graph G.
  19. This implementation is based on Kanevsky's algorithm [1]_ for finding all
  20. minimum-size node cut-sets of an undirected graph G; ie the set (or sets)
  21. of nodes of cardinality equal to the node connectivity of G. Thus if
  22. removed, would break G into two or more connected components.
  23. Parameters
  24. ----------
  25. G : NetworkX graph
  26. Undirected graph
  27. k : Integer
  28. Node connectivity of the input graph. If k is None, then it is
  29. computed. Default value: None.
  30. flow_func : function
  31. Function to perform the underlying flow computations. Default value is
  32. :func:`~networkx.algorithms.flow.edmonds_karp`. This function performs
  33. better in sparse graphs with right tailed degree distributions.
  34. :func:`~networkx.algorithms.flow.shortest_augmenting_path` will
  35. perform better in denser graphs.
  36. Returns
  37. -------
  38. cuts : a generator of node cutsets
  39. Each node cutset has cardinality equal to the node connectivity of
  40. the input graph.
  41. Examples
  42. --------
  43. >>> # A two-dimensional grid graph has 4 cutsets of cardinality 2
  44. >>> G = nx.grid_2d_graph(5, 5)
  45. >>> cutsets = list(nx.all_node_cuts(G))
  46. >>> len(cutsets)
  47. 4
  48. >>> all(2 == len(cutset) for cutset in cutsets)
  49. True
  50. >>> nx.node_connectivity(G)
  51. 2
  52. Notes
  53. -----
  54. This implementation is based on the sequential algorithm for finding all
  55. minimum-size separating vertex sets in a graph [1]_. The main idea is to
  56. compute minimum cuts using local maximum flow computations among a set
  57. of nodes of highest degree and all other non-adjacent nodes in the Graph.
  58. Once we find a minimum cut, we add an edge between the high degree
  59. node and the target node of the local maximum flow computation to make
  60. sure that we will not find that minimum cut again.
  61. See also
  62. --------
  63. node_connectivity
  64. edmonds_karp
  65. shortest_augmenting_path
  66. References
  67. ----------
  68. .. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex
  69. sets in a graph. Networks 23(6), 533--541.
  70. http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
  71. """
  72. if not nx.is_connected(G):
  73. raise nx.NetworkXError("Input graph is disconnected.")
  74. # Address some corner cases first.
  75. # For complete Graphs
  76. if nx.density(G) == 1:
  77. for cut_set in combinations(G, len(G) - 1):
  78. yield set(cut_set)
  79. return
  80. # Initialize data structures.
  81. # Keep track of the cuts already computed so we do not repeat them.
  82. seen = []
  83. # Even-Tarjan reduction is what we call auxiliary digraph
  84. # for node connectivity.
  85. H = build_auxiliary_node_connectivity(G)
  86. H_nodes = H.nodes # for speed
  87. mapping = H.graph["mapping"]
  88. # Keep a copy of original predecessors, H will be modified later.
  89. # Shallow copy is enough.
  90. original_H_pred = copy.copy(H._pred)
  91. R = build_residual_network(H, "capacity")
  92. kwargs = {"capacity": "capacity", "residual": R}
  93. # Define default flow function
  94. if flow_func is None:
  95. flow_func = default_flow_func
  96. if flow_func is shortest_augmenting_path:
  97. kwargs["two_phase"] = True
  98. # Begin the actual algorithm
  99. # step 1: Find node connectivity k of G
  100. if k is None:
  101. k = nx.node_connectivity(G, flow_func=flow_func)
  102. # step 2:
  103. # Find k nodes with top degree, call it X:
  104. X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]}
  105. # Check if X is a k-node-cutset
  106. if _is_separating_set(G, X):
  107. seen.append(X)
  108. yield X
  109. for x in X:
  110. # step 3: Compute local connectivity flow of x with all other
  111. # non adjacent nodes in G
  112. non_adjacent = set(G) - X - set(G[x])
  113. for v in non_adjacent:
  114. # step 4: compute maximum flow in an Even-Tarjan reduction H of G
  115. # and step 5: build the associated residual network R
  116. R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs)
  117. flow_value = R.graph["flow_value"]
  118. if flow_value == k:
  119. # Find the nodes incident to the flow.
  120. E1 = flowed_edges = [
  121. (u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0
  122. ]
  123. VE1 = incident_nodes = {n for edge in E1 for n in edge}
  124. # Remove saturated edges form the residual network.
  125. # Note that reversed edges are introduced with capacity 0
  126. # in the residual graph and they need to be removed too.
  127. saturated_edges = [
  128. (u, w, d)
  129. for (u, w, d) in R.edges(data=True)
  130. if d["capacity"] == d["flow"] or d["capacity"] == 0
  131. ]
  132. R.remove_edges_from(saturated_edges)
  133. R_closure = nx.transitive_closure(R)
  134. # step 6: shrink the strongly connected components of
  135. # residual flow network R and call it L.
  136. L = nx.condensation(R)
  137. cmap = L.graph["mapping"]
  138. inv_cmap = defaultdict(list)
  139. for n, scc in cmap.items():
  140. inv_cmap[scc].append(n)
  141. # Find the incident nodes in the condensed graph.
  142. VE1 = {cmap[n] for n in VE1}
  143. # step 7: Compute all antichains of L;
  144. # they map to closed sets in H.
  145. # Any edge in H that links a closed set is part of a cutset.
  146. for antichain in nx.antichains(L):
  147. # Only antichains that are subsets of incident nodes counts.
  148. # Lemma 8 in reference.
  149. if not set(antichain).issubset(VE1):
  150. continue
  151. # Nodes in an antichain of the condensation graph of
  152. # the residual network map to a closed set of nodes that
  153. # define a node partition of the auxiliary digraph H
  154. # through taking all of antichain's predecessors in the
  155. # transitive closure.
  156. S = set()
  157. for scc in antichain:
  158. S.update(inv_cmap[scc])
  159. S_ancestors = set()
  160. for n in S:
  161. S_ancestors.update(R_closure._pred[n])
  162. S.update(S_ancestors)
  163. if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S:
  164. continue
  165. # Find the cutset that links the node partition (S,~S) in H
  166. cutset = set()
  167. for u in S:
  168. cutset.update((u, w) for w in original_H_pred[u] if w not in S)
  169. # The edges in H that form the cutset are internal edges
  170. # (ie edges that represent a node of the original graph G)
  171. if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset):
  172. continue
  173. node_cut = {H_nodes[u]["id"] for u, _ in cutset}
  174. if len(node_cut) == k:
  175. # The cut is invalid if it includes internal edges of
  176. # end nodes. The other half of Lemma 8 in ref.
  177. if x in node_cut or v in node_cut:
  178. continue
  179. if node_cut not in seen:
  180. yield node_cut
  181. seen.append(node_cut)
  182. # Add an edge (x, v) to make sure that we do not
  183. # find this cutset again. This is equivalent
  184. # of adding the edge in the input graph
  185. # G.add_edge(x, v) and then regenerate H and R:
  186. # Add edges to the auxiliary digraph.
  187. # See build_residual_network for convention we used
  188. # in residual graphs.
  189. H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
  190. H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
  191. # Add edges to the residual network.
  192. R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
  193. R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0)
  194. R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
  195. R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0)
  196. # Add again the saturated edges to reuse the residual network
  197. R.add_edges_from(saturated_edges)
  198. def _is_separating_set(G, cut):
  199. """Assumes that the input graph is connected"""
  200. if len(cut) == len(G) - 1:
  201. return True
  202. H = nx.restricted_view(G, cut, [])
  203. if nx.is_connected(H):
  204. return False
  205. return True