1from itertools import chain
2
3import networkx as nx
4from networkx.utils import not_implemented_for, pairwise
5
6__all__ = ["metric_closure", "steiner_tree"]
7
8
9@not_implemented_for("directed")
10@nx._dispatchable(edge_attrs="weight", returns_graph=True)
11def metric_closure(G, weight="weight"):
12 """Return the metric closure of a graph.
13
14 The metric closure of a graph *G* is the complete graph in which each edge
15 is weighted by the shortest path distance between the nodes in *G* .
16
17 Parameters
18 ----------
19 G : NetworkX graph
20
21 Returns
22 -------
23 NetworkX graph
24 Metric closure of the graph `G`.
25
26 Notes
27 -----
28 .. deprecated:: 3.6
29 `metric_closure` is deprecated and will be removed in NetworkX 3.8.
30 Use :func:`networkx.all_pairs_shortest_path_length` instead.
31
32 """
33 import warnings
34
35 warnings.warn(
36 "metric_closure is deprecated and will be removed in NetworkX 3.8.\n"
37 "Use nx.all_pairs_shortest_path_length instead.",
38 category=DeprecationWarning,
39 stacklevel=5,
40 )
41
42 M = nx.Graph()
43
44 Gnodes = set(G)
45
46 # check for connected graph while processing first node
47 all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
48 u, (distance, path) = next(all_paths_iter)
49 if len(G) != len(distance):
50 msg = "G is not a connected graph. metric_closure is not defined."
51 raise nx.NetworkXError(msg)
52 Gnodes.remove(u)
53 for v in Gnodes:
54 M.add_edge(u, v, distance=distance[v], path=path[v])
55
56 # first node done -- now process the rest
57 for u, (distance, path) in all_paths_iter:
58 Gnodes.remove(u)
59 for v in Gnodes:
60 M.add_edge(u, v, distance=distance[v], path=path[v])
61
62 return M
63
64
65def _mehlhorn_steiner_tree(G, terminal_nodes, weight):
66 distances, paths = nx.multi_source_dijkstra(G, terminal_nodes, weight=weight)
67
68 d_1 = {}
69 s = {}
70 for v in G.nodes():
71 s[v] = paths[v][0]
72 d_1[(v, s[v])] = distances[v]
73
74 # G1-G4 names match those from the Mehlhorn 1988 paper.
75 G_1_prime = nx.Graph()
76 # iterate over all edges to complete d1
77 for u, v, data in G.edges(data=True):
78 su, sv = s[u], s[v]
79 weight_here = d_1[(u, su)] + data.get(weight, 1) + d_1[(v, sv)]
80 if not G_1_prime.has_edge(su, sv):
81 G_1_prime.add_edge(su, sv, weight_d1=weight_here)
82 else:
83 new_weight = min(weight_here, G_1_prime[su][sv]["weight_d1"])
84 G_1_prime.add_edge(su, sv, weight_d1=new_weight)
85
86 G_2 = nx.minimum_spanning_edges(G_1_prime, data=True, weight="weight_d1")
87
88 G_3 = nx.Graph()
89 for u, v, _ in G_2:
90 path = nx.shortest_path(G, u, v, weight=weight)
91 for n1, n2 in pairwise(path):
92 G_3.add_edge(n1, n2, weight=G[n1][n2].get(weight, 1))
93
94 G_3_mst = list(nx.minimum_spanning_edges(G_3, data=False, weight=weight))
95 if G.is_multigraph():
96 G_3_mst = (
97 (u, v, min(G[u][v], key=lambda k: G[u][v][k].get(weight, 1)))
98 for u, v in G_3_mst
99 )
100 G_4 = G.edge_subgraph(G_3_mst).copy()
101 _remove_nonterminal_leaves(G_4, terminal_nodes)
102 return G_4.edges()
103
104
105def _kou_steiner_tree(G, terminal_nodes, weight):
106 # Compute the metric closure only for terminal nodes
107 # Create a complete graph H from the metric edges
108 H = nx.Graph()
109 unvisited_terminals = set(terminal_nodes)
110
111 # check for connected graph while processing first node
112 u = unvisited_terminals.pop()
113 distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
114 if len(G) != len(distances):
115 msg = "G is not a connected graph."
116 raise nx.NetworkXError(msg)
117 for v in unvisited_terminals:
118 H.add_edge(u, v, distance=distances[v], path=paths[v])
119
120 # first node done -- now process the rest
121 for u in unvisited_terminals.copy():
122 distances, paths = nx.single_source_dijkstra(G, source=u, weight=weight)
123 unvisited_terminals.remove(u)
124 for v in unvisited_terminals:
125 H.add_edge(u, v, distance=distances[v], path=paths[v])
126
127 # Use the 'distance' attribute of each edge provided by H.
128 mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
129
130 # Create an iterator over each edge in each shortest path; repeats are okay
131 mst_all_edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
132 if G.is_multigraph():
133 mst_all_edges = (
134 (u, v, min(G[u][v], key=lambda k: G[u][v][k].get(weight, 1)))
135 for u, v in mst_all_edges
136 )
137
138 # Find the MST again, over this new set of edges
139 G_S = G.edge_subgraph(mst_all_edges)
140 T_S = nx.minimum_spanning_edges(G_S, weight="weight", data=False)
141
142 # Leaf nodes that are not terminal might still remain; remove them here
143 T_H = G.edge_subgraph(T_S).copy()
144 _remove_nonterminal_leaves(T_H, terminal_nodes)
145
146 return T_H.edges()
147
148
149def _remove_nonterminal_leaves(G, terminals):
150 terminal_set = set(terminals)
151 leaves = {n for n in G if len(set(G[n]) - {n}) == 1}
152 nonterminal_leaves = leaves - terminal_set
153
154 while nonterminal_leaves:
155 # Removing a node may create new non-terminal leaves, so we limit
156 # search for candidate non-terminal nodes to neighbors of current
157 # non-terminal nodes
158 candidate_leaves = set.union(*(set(G[n]) for n in nonterminal_leaves))
159 candidate_leaves -= nonterminal_leaves | terminal_set
160 # Remove current set of non-terminal nodes
161 G.remove_nodes_from(nonterminal_leaves)
162 # Find any new non-terminal nodes from the set of candidates
163 leaves = {n for n in candidate_leaves if len(set(G[n]) - {n}) == 1}
164 nonterminal_leaves = leaves - terminal_set
165
166
167ALGORITHMS = {
168 "kou": _kou_steiner_tree,
169 "mehlhorn": _mehlhorn_steiner_tree,
170}
171
172
173@not_implemented_for("directed")
174@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
175def steiner_tree(G, terminal_nodes, weight="weight", method=None):
176 r"""Return an approximation to the minimum Steiner tree of a graph.
177
178 The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes` (also *S*)
179 is a tree within `G` that spans those nodes and has minimum size (sum of
180 edge weights) among all such trees.
181
182 The approximation algorithm is specified with the `method` keyword
183 argument. All three available algorithms produce a tree whose weight is
184 within a ``(2 - (2 / l))`` factor of the weight of the optimal Steiner tree,
185 where ``l`` is the minimum number of leaf nodes across all possible Steiner
186 trees.
187
188 * ``"kou"`` [2]_ (runtime $O(|S| |V|^2)$) computes the minimum spanning tree of
189 the subgraph of the metric closure of *G* induced by the terminal nodes,
190 where the metric closure of *G* is the complete graph in which each edge is
191 weighted by the shortest path distance between the nodes in *G*.
192
193 * ``"mehlhorn"`` [3]_ (runtime $O(|E|+|V|\log|V|)$) modifies Kou et al.'s
194 algorithm, beginning by finding the closest terminal node for each
195 non-terminal. This data is used to create a complete graph containing only
196 the terminal nodes, in which edge is weighted with the shortest path
197 distance between them. The algorithm then proceeds in the same way as Kou
198 et al..
199
200 Parameters
201 ----------
202 G : NetworkX graph
203
204 terminal_nodes : list
205 A list of terminal nodes for which minimum steiner tree is
206 to be found.
207
208 weight : string (default = 'weight')
209 Use the edge attribute specified by this string as the edge weight.
210 Any edge attribute not present defaults to 1.
211
212 method : string, optional (default = 'mehlhorn')
213 The algorithm to use to approximate the Steiner tree.
214 Supported options: 'kou', 'mehlhorn'.
215 Other inputs produce a ValueError.
216
217 Returns
218 -------
219 NetworkX graph
220 Approximation to the minimum steiner tree of `G` induced by
221 `terminal_nodes` .
222
223 Raises
224 ------
225 NetworkXNotImplemented
226 If `G` is directed.
227
228 ValueError
229 If the specified `method` is not supported.
230
231 Notes
232 -----
233 For multigraphs, the edge between two nodes with minimum weight is the
234 edge put into the Steiner tree.
235
236
237 References
238 ----------
239 .. [1] Steiner_tree_problem on Wikipedia.
240 https://en.wikipedia.org/wiki/Steiner_tree_problem
241 .. [2] Kou, L., G. Markowsky, and L. Berman. 1981.
242 ‘A Fast Algorithm for Steiner Trees’.
243 Acta Informatica 15 (2): 141–45.
244 https://doi.org/10.1007/BF00288961.
245 .. [3] Mehlhorn, Kurt. 1988.
246 ‘A Faster Approximation Algorithm for the Steiner Problem in Graphs’.
247 Information Processing Letters 27 (3): 125–28.
248 https://doi.org/10.1016/0020-0190(88)90066-X.
249 """
250 if method is None:
251 method = "mehlhorn"
252
253 try:
254 algo = ALGORITHMS[method]
255 except KeyError as e:
256 raise ValueError(f"{method} is not a valid choice for an algorithm.") from e
257
258 edges = algo(G, terminal_nodes, weight)
259 # For multigraph we should add the minimal weight edge keys
260 if G.is_multigraph():
261 edges = (
262 (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
263 )
264 T = G.edge_subgraph(edges)
265 return T