weavatrix_graph/algo/steiner/
mod.rs1use crate::Vec;
2use crate::{GraphError, IndexUndirectedGraphView, Result};
3use alloc::collections::BinaryHeap;
4use core::cmp::Reverse;
5
6mod disjoint;
7mod select;
8
9use disjoint::DisjointSet;
10use select::candidate_tree;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SteinerTree<Node, Edge> {
14 terminals: Vec<Node>,
15 edges: Vec<Edge>,
16 total_cost: u64,
17}
18
19struct ShortestPaths<Edge> {
20 distance: Vec<u64>,
21 predecessor: Vec<Option<(usize, Edge)>>,
22}
23
24impl<Node, Edge> SteinerTree<Node, Edge> {
25 #[must_use]
26 pub fn terminals(&self) -> &[Node] {
27 &self.terminals
28 }
29
30 #[must_use]
31 pub fn edges(&self) -> &[Edge] {
32 &self.edges
33 }
34
35 #[must_use]
36 pub const fn total_cost(&self) -> u64 {
37 self.total_cost
38 }
39}
40
41pub fn steiner_tree_approximation<G, F>(
47 graph: &G,
48 terminals: &[G::Node],
49 edge_cost: F,
50) -> Result<Option<SteinerTree<G::Node, G::Edge>>>
51where
52 G: IndexUndirectedGraphView,
53 F: Fn(G::Edge) -> u64,
54{
55 let mut terminals = terminals.to_vec();
56 terminals.sort_unstable_by_key(|node| G::node_slot(*node));
57 terminals.dedup();
58 if terminals.iter().any(|node| !graph.contains_node(*node)) {
59 return Ok(None);
60 }
61 if terminals.len() < 2 {
62 return Ok(Some(SteinerTree {
63 terminals,
64 edges: Vec::new(),
65 total_cost: 0,
66 }));
67 }
68 let mut nodes = vec![None; graph.node_bound()];
69 for node in graph.node_indices() {
70 nodes[G::node_slot(node)] = Some(node);
71 }
72 let mut metric = Vec::new();
73 for left in 0..terminals.len() {
74 let paths = shortest_paths(graph, &nodes, terminals[left], &edge_cost)?;
75 for right in left + 1..terminals.len() {
76 let target = G::node_slot(terminals[right]);
77 let cost = paths.distance[target];
78 if cost == u64::MAX {
79 return Ok(None);
80 }
81 let Some(path) =
82 reconstruct::<G>(terminals[left], terminals[right], &paths.predecessor)
83 else {
84 return Ok(None);
85 };
86 metric.push((cost, left, right, path));
87 }
88 }
89 let mut expanded = vec![false; graph.edge_bound()];
90 for (_, _, _, path) in &metric {
91 for &edge in path {
92 expanded[G::edge_slot(edge)] = true;
93 }
94 }
95 let mut edges = candidate_tree(graph, &terminals, &expanded, &edge_cost);
96 let mut total_cost = tree_cost(&edges, &edge_cost)?;
97 for seed in 0_u64..32 {
98 let selected =
99 metric_tree_selection::<G>(&metric, graph.edge_bound(), terminals.len(), seed);
100 let candidate = candidate_tree(graph, &terminals, &selected, &edge_cost);
101 let cost = tree_cost(&candidate, &edge_cost)?;
102 if cost < total_cost {
103 edges = candidate;
104 total_cost = cost;
105 }
106 }
107 Ok(Some(SteinerTree {
108 terminals,
109 edges,
110 total_cost,
111 }))
112}
113
114fn metric_tree_selection<G>(
115 metric: &[(u64, usize, usize, Vec<G::Edge>)],
116 edge_bound: usize,
117 terminal_count: usize,
118 seed: u64,
119) -> Vec<bool>
120where
121 G: IndexUndirectedGraphView,
122{
123 let mut order = (0..metric.len()).collect::<Vec<_>>();
124 order.sort_unstable_by_key(|index| {
125 let (cost, left, right, _) = &metric[*index];
126 (*cost, metric_tie(*left, *right, terminal_count, seed))
127 });
128 let mut sets = DisjointSet::new(terminal_count);
129 let mut selected = vec![false; edge_bound];
130 for index in order {
131 let (_, left, right, path) = &metric[index];
132 if sets.union(*left, *right) {
133 for &edge in path {
134 selected[G::edge_slot(edge)] = true;
135 }
136 }
137 }
138 selected
139}
140
141fn metric_tie(left: usize, right: usize, terminal_count: usize, seed: u64) -> u64 {
142 let canonical = left
143 .checked_mul(terminal_count)
144 .and_then(|value| value.checked_add(right))
145 .and_then(|value| u64::try_from(value).ok())
146 .unwrap_or(u64::MAX);
147 if seed == 0 {
148 return canonical;
149 }
150 let mut value = canonical ^ seed.wrapping_mul(0x9E37_79B9_7F4A_7C15);
151 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
152 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
153 value ^ (value >> 31)
154}
155
156fn tree_cost<Edge: Copy, F>(edges: &[Edge], edge_cost: &F) -> Result<u64>
157where
158 F: Fn(Edge) -> u64,
159{
160 edges.iter().try_fold(0_u64, |total, edge| {
161 total
162 .checked_add(edge_cost(*edge))
163 .ok_or(GraphError::ArithmeticOverflow {
164 operation: "Steiner tree cost",
165 })
166 })
167}
168
169fn shortest_paths<G, F>(
170 graph: &G,
171 nodes: &[Option<G::Node>],
172 source: G::Node,
173 edge_cost: &F,
174) -> Result<ShortestPaths<G::Edge>>
175where
176 G: IndexUndirectedGraphView,
177 F: Fn(G::Edge) -> u64,
178{
179 let mut distance = vec![u64::MAX; graph.node_bound()];
180 let mut predecessor = vec![None; graph.node_bound()];
181 let source_slot = G::node_slot(source);
182 distance[source_slot] = 0;
183 let mut queue = BinaryHeap::from([Reverse((0_u64, source_slot))]);
184 while let Some(Reverse((cost, slot))) = queue.pop() {
185 if cost != distance[slot] {
186 continue;
187 }
188 let Some(node) = nodes[slot] else {
189 continue;
190 };
191 for edge in graph.incident_edges(node) {
192 let Some(neighbor) = graph.opposite(edge, node) else {
193 continue;
194 };
195 let candidate =
196 cost.checked_add(edge_cost(edge))
197 .ok_or(GraphError::ArithmeticOverflow {
198 operation: "Steiner Dijkstra relaxation",
199 })?;
200 let neighbor_slot = G::node_slot(neighbor);
201 if candidate < distance[neighbor_slot] {
202 distance[neighbor_slot] = candidate;
203 predecessor[neighbor_slot] = Some((slot, edge));
204 queue.push(Reverse((candidate, neighbor_slot)));
205 }
206 }
207 }
208 Ok(ShortestPaths {
209 distance,
210 predecessor,
211 })
212}
213
214fn reconstruct<G: IndexUndirectedGraphView>(
215 source: G::Node,
216 target: G::Node,
217 predecessor: &[Option<(usize, G::Edge)>],
218) -> Option<Vec<G::Edge>> {
219 let source = G::node_slot(source);
220 let mut cursor = G::node_slot(target);
221 let mut edges = Vec::new();
222 while cursor != source {
223 let (parent, edge) = predecessor[cursor]?;
224 edges.push(edge);
225 cursor = parent;
226 }
227 edges.reverse();
228 Some(edges)
229}