Skip to main content

weavatrix_graph/algo/steiner/
mod.rs

1use 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
41/// Builds a deterministic metric-closure Steiner-tree approximation.
42///
43/// # Errors
44///
45/// Returns an error when path or tree cost arithmetic overflows.
46pub 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}