Edge connectivity¶
This module implements methods for computing the edge-connectivity of graphs and digraphs. It also implements methods to extract \(k\) edge-disjoint spanning trees from a \(2k\) edge-connected graph or a \(k\) edge-connected digraph, and to pack \(k\) edge-disjoint spanning arborescences in a directed graph following the matroid approach of [Gabow1995].
Todo
Implement the tree-packing strengthening proposed in [BHKP2008]
Fix the DFS-based speed-up initialization of [GKLP2021] for digraphs with multiple edges, for which it is currently disabled
Extend to weighted digraphs
- class sage.graphs.edge_connectivity.GabowEdgeConnectivity[source]¶
Bases:
objectGabow’s algorithm for finding the edge connectivity of digraphs.
This class implements the algorithm proposed in [Gabow1995] for finding the edge connectivity of a directed graph and \(k\) edge disjoint spanning trees if the digraph is \(k\) edge connected.
Digraphs with multiple edges are supported: every parallel arc counts separately, both for the edge connectivity and for the arborescence packing. Loops are tolerated and ignored.
INPUT:
D– aDiGraph
EXAMPLES:
A random \(d\)-regular digraph is \(d\)-edge-connected:
sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = DiGraph(graphs.RandomRegular(6, 50)) # needs networkx sage: while not D.is_strongly_connected(): # needs networkx ....: D = DiGraph(graphs.RandomRegular(6, 50)) sage: GabowEdgeConnectivity(D).edge_connectivity() # needs networkx 6
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = DiGraph(graphs.RandomRegular(Integer(6), Integer(50))) # needs networkx >>> while not D.is_strongly_connected(): # needs networkx ... D = DiGraph(graphs.RandomRegular(Integer(6), Integer(50))) >>> GabowEdgeConnectivity(D).edge_connectivity() # needs networkx 6
A complete digraph with \(n\) vertices is \(n-1\)-edge-connected:
sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = DiGraph(digraphs.Complete(10)) sage: GabowEdgeConnectivity(D, use_rec=True).edge_connectivity() 9
[Python]>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = DiGraph(digraphs.Complete(Integer(10))) >>> GabowEdgeConnectivity(D, use_rec=True).edge_connectivity() 9
Check that we get the same result when with and without the DFS-based speed-up initialization proposed in [GKLP2021]:
sage: # needs networkx sage: G = graphs.RandomBarabasiAlbert(100, 2) sage: D = DiGraph(G) sage: ec1 = GabowEdgeConnectivity(D, ....: dfs_preprocessing=False).edge_connectivity() sage: ec2 = GabowEdgeConnectivity(D, ....: dfs_preprocessing=True).edge_connectivity() sage: ec3 = GabowEdgeConnectivity(D, dfs_preprocessing=True, ....: use_rec=True).edge_connectivity() sage: ec1 == ec2 and ec2 == ec3 True
>>> from sage.all import * >>> # needs networkx >>> G = graphs.RandomBarabasiAlbert(Integer(100), Integer(2)) >>> D = DiGraph(G) >>> ec1 = GabowEdgeConnectivity(D, ... dfs_preprocessing=False).edge_connectivity() >>> ec2 = GabowEdgeConnectivity(D, ... dfs_preprocessing=True).edge_connectivity() >>> ec3 = GabowEdgeConnectivity(D, dfs_preprocessing=True, ... use_rec=True).edge_connectivity() >>> ec1 == ec2 and ec2 == ec3 True
- edge_connectivity()[source]¶
Return the edge connectivity of the digraph.
Digraphs with multiple edges are supported: every parallel arc counts separately in the cuts.
EXAMPLES:
sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Complete(5) sage: GabowEdgeConnectivity(D).edge_connectivity() 4
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Complete(Integer(5)) >>> GabowEdgeConnectivity(D).edge_connectivity() 4
On a digraph with multiple edges:
sage: D = DiGraph([(0, 1), (0, 1), (1, 0)], multiedges=True) sage: GabowEdgeConnectivity(D).edge_connectivity() 1
[Python]>>> from sage.all import * >>> D = DiGraph([(Integer(0), Integer(1)), (Integer(0), Integer(1)), (Integer(1), Integer(0))], multiedges=True) >>> GabowEdgeConnectivity(D).edge_connectivity() 1
- edge_disjoint_spanning_trees(k=None, root=None, labels=False)[source]¶
Return
kedge-disjoint spanning arborescences rooted atroot.The returned arborescences are out-arborescences (rooted spanning trees with all edges directed away from the root). Following [Gabow1995] and Edmonds’ branching theorem, the maximum number of edge-disjoint spanning out-arborescences rooted at a vertex
requals the minimumr-cut of the digraph. When the digraph is strongly connected, this is at least the edge connectivityecof the digraph.INPUT:
k– integer orNone(default:None); the number of edge-disjoint spanning arborescences to return. IfNone, the method returnsself.ecarborescences (the value computed byedge_connectivity()).root– vertex of the input digraph orNone(default:None); the common root of the returned arborescences. IfNone, defaults to the first vertex of the digraph (the same convention used by the MILP backend inedge_disjoint_spanning_trees()).labels– boolean (default:False); whether the edges of the returned arborescences carry the labels of the corresponding input edges. With labels, parallel edges of a digraph with multiple edges remain distinguishable in the output: every returned edge corresponds to a distinct input edge.
OUTPUT:
A list of
DiGraph, each on the same vertex set as the input. Every returned arborescence is rooted atroot: the root has indegree 0, every other vertex has indegree 1, and every vertex is reachable fromroot. The returned arborescences are pairwise edge-disjoint.ALGORITHM:
This uses the matroid-intersection approach of [Gabow1995], in two phases. Phase 1 builds a
k-intersection whose union is ak-in-regular subgraph rooted atroot(every non-root vertex has in-degreek). Phase 2 decomposes that union intokspanning out-arborescences one at a time (find_tree): it grows the set of vertices reachable fromroot, and whenever the next edge it needs is already used by another tree, an augmenting-path swap (search_step) frees a usable edge while keeping the other trees valid.The extraction scheme follows the practical implementation studied in [GKMN2022]. Phase 1 runs in time \(O(km\log(n^2/m))\) [Gabow1995]. Phase 2 performs \(k\) extraction rounds, each rebuilding an intersection on the remaining edge pool before carving out one arborescence, and its swaps dominate the running time in practice: the observed total grows roughly like \(k^2 m\) (about \(n^4\) on complete digraphs, where \(k = n - 1\)).
EXAMPLES:
On a complete digraph, the algorithm returns
n-1arborescences:sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Complete(5) sage: trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=4) sage: len(trees) 4 sage: all(T.num_edges() == 4 for T in trees) True
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Complete(Integer(5)) >>> trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(4)) >>> len(trees) 4 >>> all(T.num_edges() == Integer(4) for T in trees) True
By default (
k=None) the method returnsecarborescences, whereecis the edge connectivity:sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Complete(5) sage: G = GabowEdgeConnectivity(D) sage: G.edge_connectivity() 4 sage: len(G.edge_disjoint_spanning_trees()) 4
[Python]>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Complete(Integer(5)) >>> G = GabowEdgeConnectivity(D) >>> G.edge_connectivity() 4 >>> len(G.edge_disjoint_spanning_trees()) 4
The arborescences can be rooted at any vertex; the root then has indegree 0, every other vertex has indegree 1, and every vertex is reachable from the root:
sage: D = digraphs.Complete(5) sage: trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=4, root=2) sage: all(T.in_degree(2) == 0 for T in trees) True sage: all(T.in_degree(v) == 1 for T in trees for v in D if v != 2) True sage: all(set(T.depth_first_search(2)) == set(D) for T in trees) True
>>> from sage.all import * >>> D = digraphs.Complete(Integer(5)) >>> trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(4), root=Integer(2)) >>> all(T.in_degree(Integer(2)) == Integer(0) for T in trees) True >>> all(T.in_degree(v) == Integer(1) for T in trees for v in D if v != Integer(2)) True >>> all(set(T.depth_first_search(Integer(2))) == set(D) for T in trees) True
The returned arborescences are pairwise edge-disjoint:
sage: D = digraphs.Complete(5) sage: trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=4) sage: all_edges = sum((T.edges(labels=False, sort=False) for T in trees), []) sage: len(all_edges) == len(set(all_edges)) True
[Python]>>> from sage.all import * >>> D = digraphs.Complete(Integer(5)) >>> trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(4)) >>> all_edges = sum((T.edges(labels=False, sort=False) for T in trees), []) >>> len(all_edges) == len(set(all_edges)) True
On a directed cycle the edge connectivity is 1, so the packing has a single arborescence: the directed path from the root:
sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Circuit(4) sage: trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(root=0) sage: len(trees) 1 sage: sorted(trees[0].edges(labels=False, sort=False)) [(0, 1), (1, 2), (2, 3)]
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Circuit(Integer(4)) >>> trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(root=Integer(0)) >>> len(trees) 1 >>> sorted(trees[Integer(0)].edges(labels=False, sort=False)) [(0, 1), (1, 2), (2, 3)]
On a digraph with multiple edges,
labels=Trueshows which copy of a parallel edge each arborescence uses; every returned edge corresponds to a distinct input edge:sage: D = DiGraph([(0, 1, 'a'), (0, 1, 'b'), (1, 0, 'c'), (1, 0, 'd')], ....: multiedges=True) sage: trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=2, root=0, labels=True) sage: sorted(e[2] for T in trees for e in T.edge_iterator()) ['a', 'b']
[Python]>>> from sage.all import * >>> D = DiGraph([(Integer(0), Integer(1), 'a'), (Integer(0), Integer(1), 'b'), (Integer(1), Integer(0), 'c'), (Integer(1), Integer(0), 'd')], ... multiedges=True) >>> trees = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(2), root=Integer(0), labels=True) >>> sorted(e[Integer(2)] for T in trees for e in T.edge_iterator()) ['a', 'b']
Trivial case:
sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Complete(5) sage: GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=0) []
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Complete(Integer(5)) >>> GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(0)) []
Asking for more arborescences than the digraph can support raises
EmptySetError:sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: from sage.categories.sets_cat import EmptySetError sage: D = digraphs.Complete(5) sage: try: ....: _ = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=5) ....: except EmptySetError: ....: print("EmptySetError") EmptySetError
[Python]>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> from sage.categories.sets_cat import EmptySetError >>> D = digraphs.Complete(Integer(5)) >>> try: ... _ = GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(5)) ... except EmptySetError: ... print("EmptySetError") EmptySetError
An unknown
rootraises aValueError:sage: from sage.graphs.edge_connectivity import GabowEdgeConnectivity sage: D = digraphs.Complete(5) sage: GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=2, root=99) Traceback (most recent call last): ... ValueError: vertex 99 is not a vertex of the graph
>>> from sage.all import * >>> from sage.graphs.edge_connectivity import GabowEdgeConnectivity >>> D = digraphs.Complete(Integer(5)) >>> GabowEdgeConnectivity(D).edge_disjoint_spanning_trees(k=Integer(2), root=Integer(99)) Traceback (most recent call last): ... ValueError: vertex 99 is not a vertex of the graph