123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- from itertools import chain
- import networkx as nx
- from networkx.utils import not_implemented_for, pairwise
- __all__ = ["metric_closure", "steiner_tree"]
- @not_implemented_for("directed")
- def metric_closure(G, weight="weight"):
- """Return the metric closure of a graph.
- The metric closure of a graph *G* is the complete graph in which each edge
- is weighted by the shortest path distance between the nodes in *G* .
- Parameters
- ----------
- G : NetworkX graph
- Returns
- -------
- NetworkX graph
- Metric closure of the graph `G`.
- """
- M = nx.Graph()
- Gnodes = set(G)
- # check for connected graph while processing first node
- all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
- u, (distance, path) = next(all_paths_iter)
- if Gnodes - set(distance):
- msg = "G is not a connected graph. metric_closure is not defined."
- raise nx.NetworkXError(msg)
- Gnodes.remove(u)
- for v in Gnodes:
- M.add_edge(u, v, distance=distance[v], path=path[v])
- # first node done -- now process the rest
- for u, (distance, path) in all_paths_iter:
- Gnodes.remove(u)
- for v in Gnodes:
- M.add_edge(u, v, distance=distance[v], path=path[v])
- return M
- def _mehlhorn_steiner_tree(G, terminal_nodes, weight):
- paths = nx.multi_source_dijkstra_path(G, terminal_nodes)
- d_1 = {}
- s = {}
- for v in G.nodes():
- s[v] = paths[v][0]
- d_1[(v, s[v])] = len(paths[v]) - 1
- # G1-G4 names match those from the Mehlhorn 1988 paper.
- G_1_prime = nx.Graph()
- for u, v, data in G.edges(data=True):
- su, sv = s[u], s[v]
- weight_here = d_1[(u, su)] + data.get(weight, 1) + d_1[(v, sv)]
- if not G_1_prime.has_edge(su, sv):
- G_1_prime.add_edge(su, sv, weight=weight_here)
- else:
- new_weight = min(weight_here, G_1_prime[su][sv][weight])
- G_1_prime.add_edge(su, sv, weight=new_weight)
- G_2 = nx.minimum_spanning_edges(G_1_prime, data=True)
- G_3 = nx.Graph()
- for u, v, d in G_2:
- path = nx.shortest_path(G, u, v, weight)
- for n1, n2 in pairwise(path):
- G_3.add_edge(n1, n2)
- G_3_mst = list(nx.minimum_spanning_edges(G_3, data=False))
- if G.is_multigraph():
- G_3_mst = (
- (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in G_3_mst
- )
- G_4 = G.edge_subgraph(G_3_mst).copy()
- _remove_nonterminal_leaves(G_4, terminal_nodes)
- return G_4.edges()
- def _kou_steiner_tree(G, terminal_nodes, weight):
- # H is the subgraph induced by terminal_nodes in the metric closure M of G.
- M = metric_closure(G, weight=weight)
- H = M.subgraph(terminal_nodes)
- # Use the 'distance' attribute of each edge provided by M.
- mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
- # Create an iterator over each edge in each shortest path; repeats are okay
- mst_all_edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
- if G.is_multigraph():
- mst_all_edges = (
- (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight]))
- for u, v in mst_all_edges
- )
- # Find the MST again, over this new set of edges
- G_S = G.edge_subgraph(mst_all_edges)
- T_S = nx.minimum_spanning_edges(G_S, weight="weight", data=False)
- # Leaf nodes that are not terminal might still remain; remove them here
- T_H = G.edge_subgraph(T_S).copy()
- _remove_nonterminal_leaves(T_H, terminal_nodes)
- return T_H.edges()
- def _remove_nonterminal_leaves(G, terminals):
- terminals_set = set(terminals)
- for n in list(G.nodes):
- if n not in terminals_set and G.degree(n) == 1:
- G.remove_node(n)
- ALGORITHMS = {
- "kou": _kou_steiner_tree,
- "mehlhorn": _mehlhorn_steiner_tree,
- }
- @not_implemented_for("directed")
- def steiner_tree(G, terminal_nodes, weight="weight", method=None):
- r"""Return an approximation to the minimum Steiner tree of a graph.
- The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes` (also *S*)
- is a tree within `G` that spans those nodes and has minimum size (sum of
- edge weights) among all such trees.
- The approximation algorithm is specified with the `method` keyword
- argument. All three available algorithms produce a tree whose weight is
- within a ``(2 - (2 / l))`` factor of the weight of the optimal Steiner tree,
- where ``l`` is the minimum number of leaf nodes across all possible Steiner
- trees.
- * ``"kou"`` [2]_ (runtime $O(|S| |V|^2)$) computes the minimum spanning tree of
- the subgraph of the metric closure of *G* induced by the terminal nodes,
- where the metric closure of *G* is the complete graph in which each edge is
- weighted by the shortest path distance between the nodes in *G*.
- * ``"mehlhorn"`` [3]_ (runtime $O(|E|+|V|\log|V|)$) modifies Kou et al.'s
- algorithm, beginning by finding the closest terminal node for each
- non-terminal. This data is used to create a complete graph containing only
- the terminal nodes, in which edge is weighted with the shortest path
- distance between them. The algorithm then proceeds in the same way as Kou
- et al..
- Parameters
- ----------
- G : NetworkX graph
- terminal_nodes : list
- A list of terminal nodes for which minimum steiner tree is
- to be found.
- weight : string (default = 'weight')
- Use the edge attribute specified by this string as the edge weight.
- Any edge attribute not present defaults to 1.
- method : string, optional (default = 'kou')
- The algorithm to use to approximate the Steiner tree.
- Supported options: 'kou', 'mehlhorn'.
- Other inputs produce a ValueError.
- Returns
- -------
- NetworkX graph
- Approximation to the minimum steiner tree of `G` induced by
- `terminal_nodes` .
- Notes
- -----
- For multigraphs, the edge between two nodes with minimum weight is the
- edge put into the Steiner tree.
- References
- ----------
- .. [1] Steiner_tree_problem on Wikipedia.
- https://en.wikipedia.org/wiki/Steiner_tree_problem
- .. [2] Kou, L., G. Markowsky, and L. Berman. 1981.
- ‘A Fast Algorithm for Steiner Trees’.
- Acta Informatica 15 (2): 141–45.
- https://doi.org/10.1007/BF00288961.
- .. [3] Mehlhorn, Kurt. 1988.
- ‘A Faster Approximation Algorithm for the Steiner Problem in Graphs’.
- Information Processing Letters 27 (3): 125–28.
- https://doi.org/10.1016/0020-0190(88)90066-X.
- """
- if method is None:
- import warnings
- msg = (
- "steiner_tree will change default method from 'kou' to 'mehlhorn'"
- "in version 3.2.\nSet the `method` kwarg to remove this warning."
- )
- warnings.warn(msg, FutureWarning, stacklevel=4)
- method = "kou"
- try:
- algo = ALGORITHMS[method]
- except KeyError as e:
- msg = f"{method} is not a valid choice for an algorithm."
- raise ValueError(msg) from e
- edges = algo(G, terminal_nodes, weight)
- # For multigraph we should add the minimal weight edge keys
- if G.is_multigraph():
- edges = (
- (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
- )
- T = G.edge_subgraph(edges)
- return T
|