Spaces:
Running
Running
File size: 7,487 Bytes
b200bda |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
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")
@nx._dispatch(edge_attrs="weight")
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")
@nx._dispatch(edge_attrs="weight")
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
|