Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/connectivity/kcutsets.py: 13%

89 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1""" 

2Kanevsky all minimum node k cutsets algorithm. 

3""" 

4import copy 

5from collections import defaultdict 

6from itertools import combinations 

7from operator import itemgetter 

8 

9import networkx as nx 

10from networkx.algorithms.flow import ( 

11 build_residual_network, 

12 edmonds_karp, 

13 shortest_augmenting_path, 

14) 

15 

16from .utils import build_auxiliary_node_connectivity 

17 

18default_flow_func = edmonds_karp 

19 

20 

21__all__ = ["all_node_cuts"] 

22 

23 

24@nx._dispatch 

25def all_node_cuts(G, k=None, flow_func=None): 

26 r"""Returns all minimum k cutsets of an undirected graph G. 

27 

28 This implementation is based on Kanevsky's algorithm [1]_ for finding all 

29 minimum-size node cut-sets of an undirected graph G; ie the set (or sets) 

30 of nodes of cardinality equal to the node connectivity of G. Thus if 

31 removed, would break G into two or more connected components. 

32 

33 Parameters 

34 ---------- 

35 G : NetworkX graph 

36 Undirected graph 

37 

38 k : Integer 

39 Node connectivity of the input graph. If k is None, then it is 

40 computed. Default value: None. 

41 

42 flow_func : function 

43 Function to perform the underlying flow computations. Default value is 

44 :func:`~networkx.algorithms.flow.edmonds_karp`. This function performs 

45 better in sparse graphs with right tailed degree distributions. 

46 :func:`~networkx.algorithms.flow.shortest_augmenting_path` will 

47 perform better in denser graphs. 

48 

49 

50 Returns 

51 ------- 

52 cuts : a generator of node cutsets 

53 Each node cutset has cardinality equal to the node connectivity of 

54 the input graph. 

55 

56 Examples 

57 -------- 

58 >>> # A two-dimensional grid graph has 4 cutsets of cardinality 2 

59 >>> G = nx.grid_2d_graph(5, 5) 

60 >>> cutsets = list(nx.all_node_cuts(G)) 

61 >>> len(cutsets) 

62 4 

63 >>> all(2 == len(cutset) for cutset in cutsets) 

64 True 

65 >>> nx.node_connectivity(G) 

66 2 

67 

68 Notes 

69 ----- 

70 This implementation is based on the sequential algorithm for finding all 

71 minimum-size separating vertex sets in a graph [1]_. The main idea is to 

72 compute minimum cuts using local maximum flow computations among a set 

73 of nodes of highest degree and all other non-adjacent nodes in the Graph. 

74 Once we find a minimum cut, we add an edge between the high degree 

75 node and the target node of the local maximum flow computation to make 

76 sure that we will not find that minimum cut again. 

77 

78 See also 

79 -------- 

80 node_connectivity 

81 edmonds_karp 

82 shortest_augmenting_path 

83 

84 References 

85 ---------- 

86 .. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex 

87 sets in a graph. Networks 23(6), 533--541. 

88 http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract 

89 

90 """ 

91 if not nx.is_connected(G): 

92 raise nx.NetworkXError("Input graph is disconnected.") 

93 

94 # Address some corner cases first. 

95 # For complete Graphs 

96 if nx.density(G) == 1: 

97 for cut_set in combinations(G, len(G) - 1): 

98 yield set(cut_set) 

99 return 

100 # Initialize data structures. 

101 # Keep track of the cuts already computed so we do not repeat them. 

102 seen = [] 

103 # Even-Tarjan reduction is what we call auxiliary digraph 

104 # for node connectivity. 

105 H = build_auxiliary_node_connectivity(G) 

106 H_nodes = H.nodes # for speed 

107 mapping = H.graph["mapping"] 

108 # Keep a copy of original predecessors, H will be modified later. 

109 # Shallow copy is enough. 

110 original_H_pred = copy.copy(H._pred) 

111 R = build_residual_network(H, "capacity") 

112 kwargs = {"capacity": "capacity", "residual": R} 

113 # Define default flow function 

114 if flow_func is None: 

115 flow_func = default_flow_func 

116 if flow_func is shortest_augmenting_path: 

117 kwargs["two_phase"] = True 

118 # Begin the actual algorithm 

119 # step 1: Find node connectivity k of G 

120 if k is None: 

121 k = nx.node_connectivity(G, flow_func=flow_func) 

122 # step 2: 

123 # Find k nodes with top degree, call it X: 

124 X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]} 

125 # Check if X is a k-node-cutset 

126 if _is_separating_set(G, X): 

127 seen.append(X) 

128 yield X 

129 

130 for x in X: 

131 # step 3: Compute local connectivity flow of x with all other 

132 # non adjacent nodes in G 

133 non_adjacent = set(G) - X - set(G[x]) 

134 for v in non_adjacent: 

135 # step 4: compute maximum flow in an Even-Tarjan reduction H of G 

136 # and step 5: build the associated residual network R 

137 R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs) 

138 flow_value = R.graph["flow_value"] 

139 

140 if flow_value == k: 

141 # Find the nodes incident to the flow. 

142 E1 = flowed_edges = [ 

143 (u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0 

144 ] 

145 VE1 = incident_nodes = {n for edge in E1 for n in edge} 

146 # Remove saturated edges form the residual network. 

147 # Note that reversed edges are introduced with capacity 0 

148 # in the residual graph and they need to be removed too. 

149 saturated_edges = [ 

150 (u, w, d) 

151 for (u, w, d) in R.edges(data=True) 

152 if d["capacity"] == d["flow"] or d["capacity"] == 0 

153 ] 

154 R.remove_edges_from(saturated_edges) 

155 R_closure = nx.transitive_closure(R) 

156 # step 6: shrink the strongly connected components of 

157 # residual flow network R and call it L. 

158 L = nx.condensation(R) 

159 cmap = L.graph["mapping"] 

160 inv_cmap = defaultdict(list) 

161 for n, scc in cmap.items(): 

162 inv_cmap[scc].append(n) 

163 # Find the incident nodes in the condensed graph. 

164 VE1 = {cmap[n] for n in VE1} 

165 # step 7: Compute all antichains of L; 

166 # they map to closed sets in H. 

167 # Any edge in H that links a closed set is part of a cutset. 

168 for antichain in nx.antichains(L): 

169 # Only antichains that are subsets of incident nodes counts. 

170 # Lemma 8 in reference. 

171 if not set(antichain).issubset(VE1): 

172 continue 

173 # Nodes in an antichain of the condensation graph of 

174 # the residual network map to a closed set of nodes that 

175 # define a node partition of the auxiliary digraph H 

176 # through taking all of antichain's predecessors in the 

177 # transitive closure. 

178 S = set() 

179 for scc in antichain: 

180 S.update(inv_cmap[scc]) 

181 S_ancestors = set() 

182 for n in S: 

183 S_ancestors.update(R_closure._pred[n]) 

184 S.update(S_ancestors) 

185 if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S: 

186 continue 

187 # Find the cutset that links the node partition (S,~S) in H 

188 cutset = set() 

189 for u in S: 

190 cutset.update((u, w) for w in original_H_pred[u] if w not in S) 

191 # The edges in H that form the cutset are internal edges 

192 # (ie edges that represent a node of the original graph G) 

193 if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset): 

194 continue 

195 node_cut = {H_nodes[u]["id"] for u, _ in cutset} 

196 

197 if len(node_cut) == k: 

198 # The cut is invalid if it includes internal edges of 

199 # end nodes. The other half of Lemma 8 in ref. 

200 if x in node_cut or v in node_cut: 

201 continue 

202 if node_cut not in seen: 

203 yield node_cut 

204 seen.append(node_cut) 

205 

206 # Add an edge (x, v) to make sure that we do not 

207 # find this cutset again. This is equivalent 

208 # of adding the edge in the input graph 

209 # G.add_edge(x, v) and then regenerate H and R: 

210 # Add edges to the auxiliary digraph. 

211 # See build_residual_network for convention we used 

212 # in residual graphs. 

213 H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1) 

214 H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1) 

215 # Add edges to the residual network. 

216 R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1) 

217 R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0) 

218 R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1) 

219 R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0) 

220 

221 # Add again the saturated edges to reuse the residual network 

222 R.add_edges_from(saturated_edges) 

223 

224 

225def _is_separating_set(G, cut): 

226 """Assumes that the input graph is connected""" 

227 if len(cut) == len(G) - 1: 

228 return True 

229 

230 H = nx.restricted_view(G, cut, []) 

231 if nx.is_connected(H): 

232 return False 

233 return True