123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- """
- Kanevsky all minimum node k cutsets algorithm.
- """
- import copy
- from collections import defaultdict
- from itertools import combinations
- from operator import itemgetter
- import networkx as nx
- from networkx.algorithms.flow import (
- build_residual_network,
- edmonds_karp,
- shortest_augmenting_path,
- )
- from .utils import build_auxiliary_node_connectivity
- default_flow_func = edmonds_karp
- __all__ = ["all_node_cuts"]
- def all_node_cuts(G, k=None, flow_func=None):
- r"""Returns all minimum k cutsets of an undirected graph G.
- This implementation is based on Kanevsky's algorithm [1]_ for finding all
- minimum-size node cut-sets of an undirected graph G; ie the set (or sets)
- of nodes of cardinality equal to the node connectivity of G. Thus if
- removed, would break G into two or more connected components.
- Parameters
- ----------
- G : NetworkX graph
- Undirected graph
- k : Integer
- Node connectivity of the input graph. If k is None, then it is
- computed. Default value: None.
- flow_func : function
- Function to perform the underlying flow computations. Default value is
- :func:`~networkx.algorithms.flow.edmonds_karp`. This function performs
- better in sparse graphs with right tailed degree distributions.
- :func:`~networkx.algorithms.flow.shortest_augmenting_path` will
- perform better in denser graphs.
- Returns
- -------
- cuts : a generator of node cutsets
- Each node cutset has cardinality equal to the node connectivity of
- the input graph.
- Examples
- --------
- >>> # A two-dimensional grid graph has 4 cutsets of cardinality 2
- >>> G = nx.grid_2d_graph(5, 5)
- >>> cutsets = list(nx.all_node_cuts(G))
- >>> len(cutsets)
- 4
- >>> all(2 == len(cutset) for cutset in cutsets)
- True
- >>> nx.node_connectivity(G)
- 2
- Notes
- -----
- This implementation is based on the sequential algorithm for finding all
- minimum-size separating vertex sets in a graph [1]_. The main idea is to
- compute minimum cuts using local maximum flow computations among a set
- of nodes of highest degree and all other non-adjacent nodes in the Graph.
- Once we find a minimum cut, we add an edge between the high degree
- node and the target node of the local maximum flow computation to make
- sure that we will not find that minimum cut again.
- See also
- --------
- node_connectivity
- edmonds_karp
- shortest_augmenting_path
- References
- ----------
- .. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex
- sets in a graph. Networks 23(6), 533--541.
- http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract
- """
- if not nx.is_connected(G):
- raise nx.NetworkXError("Input graph is disconnected.")
- # Address some corner cases first.
- # For complete Graphs
- if nx.density(G) == 1:
- for cut_set in combinations(G, len(G) - 1):
- yield set(cut_set)
- return
- # Initialize data structures.
- # Keep track of the cuts already computed so we do not repeat them.
- seen = []
- # Even-Tarjan reduction is what we call auxiliary digraph
- # for node connectivity.
- H = build_auxiliary_node_connectivity(G)
- H_nodes = H.nodes # for speed
- mapping = H.graph["mapping"]
- # Keep a copy of original predecessors, H will be modified later.
- # Shallow copy is enough.
- original_H_pred = copy.copy(H._pred)
- R = build_residual_network(H, "capacity")
- kwargs = {"capacity": "capacity", "residual": R}
- # Define default flow function
- if flow_func is None:
- flow_func = default_flow_func
- if flow_func is shortest_augmenting_path:
- kwargs["two_phase"] = True
- # Begin the actual algorithm
- # step 1: Find node connectivity k of G
- if k is None:
- k = nx.node_connectivity(G, flow_func=flow_func)
- # step 2:
- # Find k nodes with top degree, call it X:
- X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]}
- # Check if X is a k-node-cutset
- if _is_separating_set(G, X):
- seen.append(X)
- yield X
- for x in X:
- # step 3: Compute local connectivity flow of x with all other
- # non adjacent nodes in G
- non_adjacent = set(G) - X - set(G[x])
- for v in non_adjacent:
- # step 4: compute maximum flow in an Even-Tarjan reduction H of G
- # and step 5: build the associated residual network R
- R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs)
- flow_value = R.graph["flow_value"]
- if flow_value == k:
- # Find the nodes incident to the flow.
- E1 = flowed_edges = [
- (u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0
- ]
- VE1 = incident_nodes = {n for edge in E1 for n in edge}
- # Remove saturated edges form the residual network.
- # Note that reversed edges are introduced with capacity 0
- # in the residual graph and they need to be removed too.
- saturated_edges = [
- (u, w, d)
- for (u, w, d) in R.edges(data=True)
- if d["capacity"] == d["flow"] or d["capacity"] == 0
- ]
- R.remove_edges_from(saturated_edges)
- R_closure = nx.transitive_closure(R)
- # step 6: shrink the strongly connected components of
- # residual flow network R and call it L.
- L = nx.condensation(R)
- cmap = L.graph["mapping"]
- inv_cmap = defaultdict(list)
- for n, scc in cmap.items():
- inv_cmap[scc].append(n)
- # Find the incident nodes in the condensed graph.
- VE1 = {cmap[n] for n in VE1}
- # step 7: Compute all antichains of L;
- # they map to closed sets in H.
- # Any edge in H that links a closed set is part of a cutset.
- for antichain in nx.antichains(L):
- # Only antichains that are subsets of incident nodes counts.
- # Lemma 8 in reference.
- if not set(antichain).issubset(VE1):
- continue
- # Nodes in an antichain of the condensation graph of
- # the residual network map to a closed set of nodes that
- # define a node partition of the auxiliary digraph H
- # through taking all of antichain's predecessors in the
- # transitive closure.
- S = set()
- for scc in antichain:
- S.update(inv_cmap[scc])
- S_ancestors = set()
- for n in S:
- S_ancestors.update(R_closure._pred[n])
- S.update(S_ancestors)
- if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S:
- continue
- # Find the cutset that links the node partition (S,~S) in H
- cutset = set()
- for u in S:
- cutset.update((u, w) for w in original_H_pred[u] if w not in S)
- # The edges in H that form the cutset are internal edges
- # (ie edges that represent a node of the original graph G)
- if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset):
- continue
- node_cut = {H_nodes[u]["id"] for u, _ in cutset}
- if len(node_cut) == k:
- # The cut is invalid if it includes internal edges of
- # end nodes. The other half of Lemma 8 in ref.
- if x in node_cut or v in node_cut:
- continue
- if node_cut not in seen:
- yield node_cut
- seen.append(node_cut)
- # Add an edge (x, v) to make sure that we do not
- # find this cutset again. This is equivalent
- # of adding the edge in the input graph
- # G.add_edge(x, v) and then regenerate H and R:
- # Add edges to the auxiliary digraph.
- # See build_residual_network for convention we used
- # in residual graphs.
- H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
- H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
- # Add edges to the residual network.
- R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1)
- R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0)
- R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1)
- R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0)
- # Add again the saturated edges to reuse the residual network
- R.add_edges_from(saturated_edges)
- def _is_separating_set(G, cut):
- """Assumes that the input graph is connected"""
- if len(cut) == len(G) - 1:
- return True
- H = nx.restricted_view(G, cut, [])
- if nx.is_connected(H):
- return False
- return True
|