Skip to content

Commit 712ead9

Browse files
author
Andrea Tomassilli
committed
rebase + clean
1 parent ad25ce0 commit 712ead9

2 files changed

Lines changed: 130 additions & 56 deletions

File tree

networkx/algorithms/approximation/kcutsets.py

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Functions involving natural generalizations of the minimum cut problem."""
22

33
import itertools
4+
45
import networkx as nx
56
from networkx.utils import not_implemented_for
67

@@ -9,8 +10,9 @@
910

1011
@not_implemented_for("directed")
1112
@not_implemented_for("multigraph")
13+
@nx._dispatchable(edge_attrs="weight")
1214
def minimum_multiway_cut(G, terminals, weight=None):
13-
"""Compute an approximated Minimum Multiway Cut and the corresponding cut value.
15+
r"""Compute an approximated Minimum Multiway Cut and the corresponding cut value.
1416
1517
Given an undirected graph $G = (V, E)$ and a set of terminals
1618
$S = \{s_1, s_2, \dots ,s_k\} \subseteq V$, a multiway cut is a set of edges
@@ -40,8 +42,8 @@ def minimum_multiway_cut(G, terminals, weight=None):
4042
A container with a subset of nodes of G.
4143
4244
weight : string, optional (default = None)
43-
If None, every node has weight 1. If a string, use this node
44-
attribute as the node weight. A node without this attribute is
45+
If None, every edge has weight 1. If a string, use this edge
46+
attribute as the edge weight. An edge without this attribute is
4547
assumed to have weight 1.
4648
4749
Returns
@@ -51,7 +53,27 @@ def minimum_multiway_cut(G, terminals, weight=None):
5153
5254
cutset : set
5355
Set of edges that, if removed from the graph, disconnects each
54-
terminal from all the others.
56+
terminal from all the others.
57+
58+
Examples
59+
--------
60+
>>> G = nx.path_graph(5)
61+
>>> terminals = [0, 4]
62+
>>> cut_value, cutset = nx.approximation.minimum_multiway_cut(G, terminals)
63+
>>> cut_value
64+
1
65+
>>> len(cutset)
66+
1
67+
68+
For a weighted graph:
69+
70+
>>> G = nx.Graph()
71+
>>> G.add_weighted_edges_from([(0, 1, 10), (1, 2, 5), (2, 3, 10)])
72+
>>> cut_value, cutset = nx.approximation.minimum_multiway_cut(
73+
... G, [0, 3], weight="weight"
74+
... )
75+
>>> cut_value
76+
5
5577
5678
Raises
5779
------
@@ -73,56 +95,45 @@ def minimum_multiway_cut(G, terminals, weight=None):
7395
raise nx.NetworkXError("Expected non-empty NetworkX graph!")
7496
if not nx.is_connected(G):
7597
raise nx.NetworkXError("Graph not connected.")
76-
# only consider the terminals in G
7798
terminals = set(terminals) & G.nodes()
78-
# raise an error if less than two terminal have been provided
7999
if len(terminals) < 2:
80100
raise nx.NetworkXError("At least two terminals should be provided.")
81101

82-
# extract edges weight, and set edges weights with no attribute to 1
83-
edges_weights = G.edges(data=weight, default=1)
84-
# create a new Graph G2
102+
# Build working graph with uniform weight attribute name
85103
G2 = nx.Graph()
86-
G2.add_weighted_edges_from(edges_weights, weight="capacity")
104+
G2.add_weighted_edges_from(G.edges(data=weight, default=1), weight="capacity")
87105

88-
# take a non-existing node of G to be the sink
106+
# Create auxiliary sink connected to all terminals with infinite capacity
89107
sink = next(u for u in range(len(G) + 1) if u not in G)
90-
# add edges from the terminals to the sink node with infinite capacity
91108
G2.add_edges_from([(u, sink) for u in terminals], capacity=float("inf"))
92109

110+
# Compute minimum isolating cut for each terminal
93111
all_cuts = []
94-
# compute the minimum weight isolating cut for each terminal
95112
for u in terminals:
96-
# remove the edge from u to the sink
97113
G2.remove_edge(u, sink)
98-
# get the cut value and the 2 partitions of nodes
99114
value_cut, (p1, p2) = nx.minimum_cut(G2, u, sink)
100-
# get the edges crossing the cut
101115
edges_cut = set(nx.edge_boundary(G2, p1, p2))
102-
# add to the result
103116
all_cuts.append((edges_cut, value_cut))
104-
# re-add the edge to the sink
105117
G2.add_edge(u, sink, capacity=float("inf"))
106118

107-
# discard the heaviest cut and take the union of the rest
119+
# Discard heaviest cut and union the rest (avoiding duplicate edges)
108120
cutset = set()
109121
cut_value = 0
110122
for u, v in itertools.chain.from_iterable(
111123
el[0] for el in sorted(all_cuts, key=lambda x: x[1])[:-1]
112124
):
113125
if (u, v) not in cutset and (v, u) not in cutset:
114-
# add to the cutset
115126
cutset.add((u, v))
116-
# add the weight to the cut cost
117127
cut_value += G2.edges[u, v]["capacity"]
118128

119129
return cut_value, cutset
120130

121131

122132
@not_implemented_for("directed")
123133
@not_implemented_for("multigraph")
134+
@nx._dispatchable(edge_attrs="weight")
124135
def minimum_k_cut(G, k, weight=None):
125-
"""Compute an approximated Minimum k-Cut and the corresponding cut value.
136+
r"""Compute an approximated Minimum k-Cut and the corresponding cut value.
126137
127138
Given an undirected graph $G = (V, E)$ and an integer $k \geq 2$, a $k$-cut is a
128139
set of edges whose removal leaves at least $k$ connected components.
@@ -180,6 +191,24 @@ def minimum_k_cut(G, k, weight=None):
180191
If more than $k$ components are created then it's enough to throw back
181192
some of the removed edges until there are exactly $k$ components.
182193
194+
Examples
195+
--------
196+
>>> G = nx.path_graph(5)
197+
>>> cut_value, cutset = nx.approximation.minimum_k_cut(G, k=3)
198+
>>> cut_value
199+
2
200+
>>> G.remove_edges_from(cutset)
201+
>>> len(list(nx.connected_components(G))) >= 3
202+
True
203+
204+
For a weighted graph:
205+
206+
>>> G = nx.complete_graph(4)
207+
>>> nx.set_edge_attributes(G, values=10, name="weight")
208+
>>> cut_value, cutset = nx.approximation.minimum_k_cut(G, k=4, weight="weight")
209+
>>> cut_value
210+
60
211+
183212
See also
184213
--------
185214
networkx.algorithms.flow.minimum_cut
@@ -197,37 +226,28 @@ def minimum_k_cut(G, k, weight=None):
197226
if not 1 <= k <= len(G):
198227
raise nx.NetworkXError(f"k should be within 1 and {len(G)}")
199228

200-
# extract edges weights, and set edges weights with no attribute to 1
201-
edges_weights = G.edges(data=weight, default=1)
202-
# create a new Graph G2
229+
# Build working graph with uniform weight attribute name
203230
G2 = nx.Graph()
204-
G2.add_weighted_edges_from(edges_weights, weight="capacity")
231+
G2.add_weighted_edges_from(G.edges(data=weight, default=1), weight="capacity")
205232

206-
# build a Gomory-Hu tree T from G
233+
# Build Gomory-Hu tree and get k-1 lightest edges
207234
T = nx.gomory_hu_tree(G2)
208-
# get the k-1 cheapest edges of the Gomory-Hu tree
209235
min_weight_edges = sorted(T.edges(data="weight"), key=lambda x: x[2])[: k - 1]
210236

211-
# compute the cutset, the value is computed after as an edge might appear more than once
237+
# Collect boundary edges for each cut induced by removing tree edges
212238
all_edges_cut = set()
213239
for u, v, _ in min_weight_edges:
214-
# remove (u,v) from the tree
215240
T.remove_edge(u, v)
216-
# get the connected component that contains u
217241
p1 = nx.node_connected_component(T, u)
218-
# add the boundary edges of p1 to the cutset
219242
all_edges_cut |= set(nx.edge_boundary(G2, p1))
220-
# re-add (u,v) to the tree
221243
T.add_edge(u, v)
222244

223-
# consider edges only in a direction
245+
# Compute final cutset avoiding duplicate edges
224246
cutset = set()
225247
cut_value = 0
226248
for u, v in all_edges_cut:
227249
if (u, v) not in cutset and (v, u) not in cutset:
228-
# add to the cutset
229250
cutset.add((u, v))
230-
# add the weight to the cut cost
231251
cut_value += G2.edges[u, v]["capacity"]
232252

233253
return cut_value, cutset

networkx/algorithms/approximation/tests/test_kcutsets.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Unit tests for the :mod:`networkx.algorithms.approximation.kcutsets` module."""
22

33
import itertools
4+
45
import pytest
6+
57
import networkx as nx
6-
from networkx.algorithms.approximation import minimum_multiway_cut, minimum_k_cut
7-
from networkx import minimum_cut_value
8+
from networkx.algorithms import approximation as approx
89

910

1011
class TestMinMultiwayCut:
@@ -18,14 +19,14 @@ def test_null_graph(self):
1819
with pytest.raises(
1920
nx.NetworkXError, match="Expected non-empty NetworkX graph!"
2021
):
21-
minimum_multiway_cut(G, G.nodes())
22+
approx.minimum_multiway_cut(G, G.nodes())
2223

2324
def test_undirected_non_connected(self):
2425
"""Test an undirected disconnected graph."""
2526
G = nx.path_graph(10)
2627
G.remove_edge(3, 4)
2728
with pytest.raises(nx.NetworkXError, match="Graph not connected."):
28-
minimum_multiway_cut(G, G.nodes())
29+
approx.minimum_multiway_cut(G, G.nodes())
2930

3031
def test_invalid_terminals(self):
3132
"""Test empty terminals."""
@@ -34,12 +35,12 @@ def test_invalid_terminals(self):
3435
with pytest.raises(
3536
nx.NetworkXError, match="At least two terminals should be provided."
3637
):
37-
minimum_multiway_cut(G, [])
38+
approx.minimum_multiway_cut(G, [])
3839

3940
def test_path_graph_unweighted(self):
4041
"""Test min multiway cut for a path graph."""
4142
G = nx.path_graph(2)
42-
cut_value, cutset = minimum_multiway_cut(G, [0, 1])
43+
cut_value, cutset = approx.minimum_multiway_cut(G, [0, 1])
4344
assert cut_value == 1
4445
G.remove_edges_from(cutset)
4546
assert len(list(nx.connected_components(G))) == 2
@@ -50,18 +51,50 @@ def test_path_graph_weighted(self):
5051
G.add_weighted_edges_from(
5152
[(0, 1, 10), (1, 2, 10), (2, 3, 5)], weight="capacity"
5253
)
53-
cut_value, cutset = minimum_multiway_cut(G, [0, 3], weight="capacity")
54+
cut_value, cutset = approx.minimum_multiway_cut(G, [0, 3], weight="capacity")
5455
assert cut_value == 5
5556

5657
def test_complete_graph(self):
5758
"""Test min multiway cut for a complete graph."""
5859
G = nx.complete_graph(5)
59-
cut_value, cutset = minimum_multiway_cut(G, G.nodes())
60+
cut_value, cutset = approx.minimum_multiway_cut(G, G.nodes())
6061
assert cut_value == 10
6162
# remove the edges
6263
G.remove_edges_from(cutset)
6364
assert set(G.edges()) == set()
6465

66+
def test_single_terminal(self):
67+
"""Test that a single terminal raises an error."""
68+
G = nx.path_graph(5)
69+
with pytest.raises(
70+
nx.NetworkXError, match="At least two terminals should be provided."
71+
):
72+
approx.minimum_multiway_cut(G, [0])
73+
74+
def test_terminals_not_in_graph(self):
75+
"""Test terminals that are not in the graph are ignored."""
76+
G = nx.path_graph(5)
77+
# terminals 100 and 200 don't exist, only 0 and 4 are valid
78+
cut_value, cutset = approx.minimum_multiway_cut(G, [0, 4, 100, 200])
79+
assert cut_value == 1
80+
G.remove_edges_from(cutset)
81+
components = list(nx.connected_components(G))
82+
assert len(components) == 2
83+
84+
def test_directed_graph_raises(self):
85+
"""Test that directed graphs raise NetworkXNotImplemented."""
86+
G = nx.DiGraph()
87+
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
88+
with pytest.raises(nx.NetworkXNotImplemented):
89+
approx.minimum_multiway_cut(G, [0, 3])
90+
91+
def test_multigraph_raises(self):
92+
"""Test that multigraphs raise NetworkXNotImplemented."""
93+
G = nx.MultiGraph()
94+
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
95+
with pytest.raises(nx.NetworkXNotImplemented):
96+
approx.minimum_multiway_cut(G, [0, 3])
97+
6598
@pytest.mark.parametrize(
6699
"graph_class",
67100
[
@@ -82,8 +115,8 @@ def test_compare_min_cut(self, graph_class, s, t):
82115
"""
83116
G = graph_class()
84117
nx.set_edge_attributes(G, values=10, name="weight")
85-
cut_value, cutset = minimum_multiway_cut(G, {s, t}, weight="weight")
86-
assert cut_value == minimum_cut_value(G, s, t, capacity="weight")
118+
cut_value, cutset = approx.minimum_multiway_cut(G, {s, t}, weight="weight")
119+
assert cut_value == nx.minimum_cut_value(G, s, t, capacity="weight")
87120

88121

89122
class TestMinkCut:
@@ -97,28 +130,49 @@ def test_null_graph(self):
97130
with pytest.raises(
98131
nx.NetworkXError, match="Expected non-empty NetworkX graph!"
99132
):
100-
minimum_k_cut(G, 3)
133+
approx.minimum_k_cut(G, 3)
101134

102135
def test_undirected_non_connected(self):
103136
"""Test an undirected disconnected graph."""
104137
G = nx.path_graph(10)
105138
G.remove_edge(3, 4)
106139
with pytest.raises(nx.NetworkXError, match="Graph not connected."):
107-
minimum_k_cut(G, 3)
140+
approx.minimum_k_cut(G, 3)
108141

109142
def test_invalid_k(self):
110-
"""Test empty terminals."""
143+
"""Test invalid k values."""
111144
G = nx.path_graph(10)
112145
with pytest.raises(nx.NetworkXError, match="k should be within 1 and 10"):
113-
minimum_k_cut(G, 0)
146+
approx.minimum_k_cut(G, 0)
114147
with pytest.raises(nx.NetworkXError, match="k should be within 1 and 10"):
115-
minimum_k_cut(G, 11)
148+
approx.minimum_k_cut(G, 11)
149+
150+
def test_directed_graph_raises(self):
151+
"""Test that directed graphs raise NetworkXNotImplemented."""
152+
G = nx.DiGraph()
153+
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
154+
with pytest.raises(nx.NetworkXNotImplemented):
155+
approx.minimum_k_cut(G, 2)
156+
157+
def test_multigraph_raises(self):
158+
"""Test that multigraphs raise NetworkXNotImplemented."""
159+
G = nx.MultiGraph()
160+
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
161+
with pytest.raises(nx.NetworkXNotImplemented):
162+
approx.minimum_k_cut(G, 2)
163+
164+
def test_k_equals_one(self):
165+
"""Test k=1 returns empty cutset with zero cut value."""
166+
G = nx.complete_graph(5)
167+
cut_value, cutset = approx.minimum_k_cut(G, 1)
168+
assert cut_value == 0
169+
assert len(cutset) == 0
116170

117171
@pytest.mark.parametrize("k,expected", [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4)])
118172
def test_path_graph(self, k, expected):
119173
"""Test various k for a path graph of 5 nodes."""
120174
G = nx.path_graph(n=5)
121-
cut_value, cutset = minimum_k_cut(G, k, weight="capacity")
175+
cut_value, cutset = approx.minimum_k_cut(G, k, weight="capacity")
122176
assert cut_value == expected
123177
G.remove_edges_from(cutset)
124178
assert len(list(nx.connected_components(G))) == k
@@ -127,7 +181,7 @@ def test_path_graph(self, k, expected):
127181
def test_complete_graph(self, k, expected):
128182
"""Test various k for a complete graph of 5 nodes."""
129183
G = nx.complete_graph(5)
130-
cut_value, cutset = minimum_k_cut(G, k)
184+
cut_value, cutset = approx.minimum_k_cut(G, k)
131185
assert cut_value == expected
132186
G.remove_edges_from(cutset)
133187
assert len(list(nx.connected_components(G))) >= k
@@ -147,15 +201,15 @@ def test_complete_graph(self, k, expected):
147201
def test_connected_components(self, graph_class, k):
148202
"""Test multiple graph types and k."""
149203
G = graph_class()
150-
cut_value, cutset = minimum_k_cut(G, k)
204+
cut_value, cutset = approx.minimum_k_cut(G, k)
151205
G.remove_edges_from(cutset)
152206
assert len(list(nx.connected_components(G))) >= k
153207

154208
def test_complete_graph_weighted(self):
155209
"""Test min k-cut for a weighted complete graph."""
156210
G = nx.complete_graph(5)
157211
nx.set_edge_attributes(G, values=10, name="weight")
158-
cut_value, cutset = minimum_k_cut(G, 5, weight="weight")
212+
cut_value, cutset = approx.minimum_k_cut(G, 5, weight="weight")
159213
assert cut_value == 100
160214
# remove the edges
161215
G.remove_edges_from(cutset)
@@ -167,7 +221,7 @@ def test_path_graph_weighted(self):
167221
G.add_weighted_edges_from(
168222
[(0, 1, 10), (1, 2, 10), (2, 3, 5)], weight="capacity"
169223
)
170-
cut_value, cutset = minimum_k_cut(G, 3, weight="capacity")
224+
cut_value, cutset = approx.minimum_k_cut(G, 3, weight="capacity")
171225
assert cut_value == 15
172226
G.remove_edges_from(cutset)
173227
assert len(list(nx.connected_components(G))) == 3

0 commit comments

Comments
 (0)