Skip to main content

oxicuda_graphalg/shortest_path/
delta_stepping.rs

1//! Delta-stepping single-source shortest paths (Meyer & Sanders 2003).
2//!
3//! Delta-stepping is a bucket-based label-correcting SSSP algorithm that bridges
4//! Dijkstra (`Δ → 0`, fully label-setting) and Bellman-Ford (`Δ → ∞`, fully
5//! label-correcting). It groups tentative distances into buckets of width `Δ`
6//! and processes the lowest non-empty bucket at a time, which exposes the
7//! parallelism exploited by GPU/cluster implementations while remaining a clean
8//! sequential algorithm here.
9//!
10//! ## Algorithm
11//!
12//! Edges are classified as **light** (`weight ≤ Δ`) or **heavy** (`weight > Δ`).
13//! For the current bucket `B[i]`:
14//!
15//! 1. Repeatedly relax all *light* edges out of the bucket's vertices,
16//!    re-inserting any vertex that lands back in `B[i]` (this settles all
17//!    vertices whose final distance lies in `[iΔ, (i+1)Δ)`).
18//! 2. Then relax all *heavy* edges out of the vertices that were removed from
19//!    `B[i]` during step 1 (these can only target later buckets).
20//!
21//! A relaxation `relax(v, x)` moves `v` from its old bucket to bucket
22//! `⌊x / Δ⌋` whenever `x` improves `tent[v]`.
23//!
24//! Rejects negative edge weights (buckets assume non-negative distances), like
25//! Dijkstra.
26//!
27//! ## References
28//! - Meyer, U. & Sanders, P. (2003). "Δ-stepping: a parallelizable shortest path
29//!   algorithm." Journal of Algorithms 49(1), 114–152.
30
31use std::collections::HashSet;
32
33use crate::error::{GraphalgError, GraphalgResult};
34use crate::repr::weighted_graph::WeightedGraph;
35
36/// Result of a delta-stepping run from a single source.
37#[derive(Debug, Clone)]
38pub struct DeltaSteppingOutput {
39    /// `dist[v]` = shortest-path distance from the source (∞ if unreachable).
40    pub dist: Vec<f64>,
41    /// `parent[v]` = predecessor of `v` on a shortest path (`usize::MAX` if none;
42    /// the source is its own parent).
43    pub parent: Vec<usize>,
44}
45
46/// Compute single-source shortest paths via delta-stepping with bucket width `delta`.
47///
48/// # Errors
49/// - [`GraphalgError::SourceOutOfRange`] if `source >= g.n`.
50/// - [`GraphalgError::InvalidParameter`] if `delta` is not finite and positive.
51/// - [`GraphalgError::NegativeWeight`] if any edge weight is negative.
52pub fn delta_stepping(
53    g: &WeightedGraph,
54    source: usize,
55    delta: f64,
56) -> GraphalgResult<DeltaSteppingOutput> {
57    if source >= g.n {
58        return Err(GraphalgError::SourceOutOfRange {
59            node: source,
60            n: g.n,
61        });
62    }
63    if !(delta.is_finite() && delta > 0.0) {
64        return Err(GraphalgError::InvalidParameter(format!(
65            "delta must be finite and > 0, got {delta}"
66        )));
67    }
68    // Validate non-negativity up front.
69    for (u, adj) in g.edges.iter().enumerate() {
70        for &(v, w) in adj {
71            if w < 0.0 {
72                return Err(GraphalgError::NegativeWeight {
73                    edge: (u, v),
74                    weight: w,
75                });
76            }
77        }
78    }
79
80    let n = g.n;
81    let mut tent = vec![f64::INFINITY; n];
82    let mut parent = vec![usize::MAX; n];
83    // The bucket index each vertex currently sits in (usize::MAX = none).
84    let mut bucket_of = vec![usize::MAX; n];
85    // Sparse bucket array: index → set of vertices.
86    let mut buckets: Vec<HashSet<usize>> = Vec::new();
87
88    // relax(v, x, p): place v into the correct bucket if x improves tent[v].
89    // Implemented inline (closures can't easily borrow all of these mutably).
90    fn relax(
91        v: usize,
92        x: f64,
93        p: usize,
94        delta: f64,
95        tent: &mut [f64],
96        parent: &mut [usize],
97        bucket_of: &mut [usize],
98        buckets: &mut Vec<HashSet<usize>>,
99    ) {
100        if x < tent[v] {
101            // Remove from its old bucket, if any.
102            let old = bucket_of[v];
103            if old != usize::MAX {
104                if let Some(b) = buckets.get_mut(old) {
105                    b.remove(&v);
106                }
107            }
108            let new_idx = (x / delta).floor() as usize;
109            if new_idx >= buckets.len() {
110                buckets.resize(new_idx + 1, HashSet::new());
111            }
112            buckets[new_idx].insert(v);
113            bucket_of[v] = new_idx;
114            tent[v] = x;
115            parent[v] = p;
116        }
117    }
118
119    // `relax` performs the `x < tent[v]` test, so leave `tent[source]` at ∞ and
120    // let the initial relaxation set it to 0 and insert it into bucket 0.
121    relax(
122        source,
123        0.0,
124        source,
125        delta,
126        &mut tent,
127        &mut parent,
128        &mut bucket_of,
129        &mut buckets,
130    );
131
132    let mut i = 0usize;
133    while i < buckets.len() {
134        // Skip empty buckets.
135        if buckets[i].is_empty() {
136            i += 1;
137            continue;
138        }
139
140        // `settled` collects every vertex removed from B[i] across the light
141        // phase; their heavy edges are relaxed afterwards.
142        let mut settled: Vec<usize> = Vec::new();
143
144        // ── Light-edge phase: iterate until B[i] is empty ────────────────────
145        while !buckets[i].is_empty() {
146            // Snapshot the current contents of B[i] and clear it.
147            let current: Vec<usize> = buckets[i].drain().collect();
148            for &u in &current {
149                // Mark as removed from this bucket for the heavy phase.
150                bucket_of[u] = usize::MAX;
151                settled.push(u);
152            }
153            // Relax light edges; vertices may re-enter B[i].
154            for &u in &current {
155                let du = tent[u];
156                for &(v, w) in g.neighbors(u)? {
157                    if w <= delta {
158                        relax(
159                            v,
160                            du + w,
161                            u,
162                            delta,
163                            &mut tent,
164                            &mut parent,
165                            &mut bucket_of,
166                            &mut buckets,
167                        );
168                    }
169                }
170            }
171        }
172
173        // ── Heavy-edge phase: relax heavy edges from settled vertices ────────
174        for &u in &settled {
175            let du = tent[u];
176            for &(v, w) in g.neighbors(u)? {
177                if w > delta {
178                    relax(
179                        v,
180                        du + w,
181                        u,
182                        delta,
183                        &mut tent,
184                        &mut parent,
185                        &mut bucket_of,
186                        &mut buckets,
187                    );
188                }
189            }
190        }
191        // Do not advance `i`: heavy relaxations may have re-filled B[i] with a
192        // vertex whose improved distance landed back in this bucket. Loop again;
193        // it terminates because distances only decrease and are bounded below.
194        if buckets[i].is_empty() {
195            i += 1;
196        }
197    }
198
199    Ok(DeltaSteppingOutput { dist: tent, parent })
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::shortest_path::dijkstra::dijkstra;
206
207    #[test]
208    fn simple_chain() {
209        // 0 ->1(1) ->2(1) ->3(1).
210        let mut g = WeightedGraph::new(4);
211        g.add_edge(0, 1, 1.0).expect("ok");
212        g.add_edge(1, 2, 1.0).expect("ok");
213        g.add_edge(2, 3, 1.0).expect("ok");
214        let out = delta_stepping(&g, 0, 1.0).expect("ok");
215        assert!((out.dist[3] - 3.0).abs() < 1e-12);
216    }
217
218    #[test]
219    fn matches_dijkstra_small() {
220        // 0->1(1), 0->2(4), 1->2(2), 1->3(5), 2->3(1).
221        let mut g = WeightedGraph::new(4);
222        g.add_edge(0, 1, 1.0).expect("ok");
223        g.add_edge(0, 2, 4.0).expect("ok");
224        g.add_edge(1, 2, 2.0).expect("ok");
225        g.add_edge(1, 3, 5.0).expect("ok");
226        g.add_edge(2, 3, 1.0).expect("ok");
227        let dj = dijkstra(&g, 0).expect("ok");
228        for &delta in &[0.5_f64, 1.0, 2.0, 10.0] {
229            let ds = delta_stepping(&g, 0, delta).expect("ok");
230            for v in 0..g.n {
231                assert!(
232                    (ds.dist[v] - dj.dist[v]).abs() < 1e-9,
233                    "delta={delta} v={v}: {} vs {}",
234                    ds.dist[v],
235                    dj.dist[v]
236                );
237            }
238        }
239    }
240
241    #[test]
242    fn matches_dijkstra_undirected_grid() {
243        // A small undirected weighted graph with mixed light/heavy edges.
244        let mut g = WeightedGraph::new(6);
245        g.add_undirected_edge(0, 1, 0.3).expect("ok");
246        g.add_undirected_edge(1, 2, 2.5).expect("ok");
247        g.add_undirected_edge(0, 3, 1.0).expect("ok");
248        g.add_undirected_edge(3, 4, 0.7).expect("ok");
249        g.add_undirected_edge(4, 5, 3.2).expect("ok");
250        g.add_undirected_edge(2, 5, 0.4).expect("ok");
251        g.add_undirected_edge(1, 4, 1.1).expect("ok");
252        let dj = dijkstra(&g, 0).expect("ok");
253        let ds = delta_stepping(&g, 0, 1.0).expect("ok");
254        for v in 0..g.n {
255            assert!(
256                (ds.dist[v] - dj.dist[v]).abs() < 1e-9,
257                "v={v}: {} vs {}",
258                ds.dist[v],
259                dj.dist[v]
260            );
261        }
262    }
263
264    #[test]
265    fn source_distance_zero() {
266        let mut g = WeightedGraph::new(3);
267        g.add_edge(0, 1, 2.0).expect("ok");
268        let out = delta_stepping(&g, 0, 1.0).expect("ok");
269        assert!((out.dist[0] - 0.0).abs() < 1e-12);
270        assert_eq!(out.parent[0], 0);
271    }
272
273    #[test]
274    fn unreachable_is_infinity() {
275        let mut g = WeightedGraph::new(3);
276        g.add_edge(0, 1, 1.0).expect("ok");
277        // Node 2 is unreachable.
278        let out = delta_stepping(&g, 0, 1.0).expect("ok");
279        assert!(out.dist[2].is_infinite());
280        assert_eq!(out.parent[2], usize::MAX);
281    }
282
283    #[test]
284    fn rejects_negative_weight() {
285        let mut g = WeightedGraph::new(2);
286        g.add_edge(0, 1, -1.0).expect("ok");
287        assert!(matches!(
288            delta_stepping(&g, 0, 1.0).unwrap_err(),
289            GraphalgError::NegativeWeight { .. }
290        ));
291    }
292
293    #[test]
294    fn rejects_bad_delta() {
295        let mut g = WeightedGraph::new(2);
296        g.add_edge(0, 1, 1.0).expect("ok");
297        assert!(delta_stepping(&g, 0, 0.0).is_err());
298        assert!(delta_stepping(&g, 0, -1.0).is_err());
299        assert!(delta_stepping(&g, 0, f64::INFINITY).is_err());
300    }
301
302    #[test]
303    fn source_out_of_range() {
304        let g = WeightedGraph::new(3);
305        assert!(matches!(
306            delta_stepping(&g, 5, 1.0).unwrap_err(),
307            GraphalgError::SourceOutOfRange { .. }
308        ));
309    }
310
311    #[test]
312    fn parent_pointers_form_tree() {
313        // Each reachable non-source vertex must have a valid parent whose
314        // distance + edge weight equals the vertex's distance.
315        let mut g = WeightedGraph::new(5);
316        g.add_edge(0, 1, 2.0).expect("ok");
317        g.add_edge(0, 2, 5.0).expect("ok");
318        g.add_edge(1, 2, 1.0).expect("ok");
319        g.add_edge(2, 3, 2.0).expect("ok");
320        g.add_edge(1, 4, 7.0).expect("ok");
321        let out = delta_stepping(&g, 0, 1.5).expect("ok");
322        for v in 0..g.n {
323            if v == 0 || out.dist[v].is_infinite() {
324                continue;
325            }
326            let p = out.parent[v];
327            assert!(p != usize::MAX, "v={v} has no parent");
328            // Find the edge (p, v).
329            let w = g
330                .neighbors(p)
331                .expect("ok")
332                .iter()
333                .find(|&&(t, _)| t == v)
334                .map(|&(_, w)| w)
335                .expect("parent edge exists");
336            assert!(
337                (out.dist[p] + w - out.dist[v]).abs() < 1e-9,
338                "tree broken at v={v}"
339            );
340        }
341    }
342
343    #[test]
344    fn large_delta_acts_like_bellman_ford() {
345        // With Δ larger than any edge, all edges are "light"; result must still
346        // be correct.
347        let mut g = WeightedGraph::new(4);
348        g.add_edge(0, 1, 1.0).expect("ok");
349        g.add_edge(1, 2, 1.0).expect("ok");
350        g.add_edge(0, 2, 5.0).expect("ok");
351        g.add_edge(2, 3, 1.0).expect("ok");
352        let dj = dijkstra(&g, 0).expect("ok");
353        let ds = delta_stepping(&g, 0, 1000.0).expect("ok");
354        for v in 0..g.n {
355            assert!((ds.dist[v] - dj.dist[v]).abs() < 1e-9, "v={v}");
356        }
357    }
358
359    #[test]
360    fn small_delta_acts_like_dijkstra() {
361        // Tiny Δ forces many buckets (label-setting behaviour).
362        let mut g = WeightedGraph::new(4);
363        g.add_edge(0, 1, 1.0).expect("ok");
364        g.add_edge(0, 2, 4.0).expect("ok");
365        g.add_edge(1, 2, 2.0).expect("ok");
366        g.add_edge(2, 3, 1.0).expect("ok");
367        let dj = dijkstra(&g, 0).expect("ok");
368        let ds = delta_stepping(&g, 0, 0.1).expect("ok");
369        for v in 0..g.n {
370            assert!((ds.dist[v] - dj.dist[v]).abs() < 1e-9, "v={v}");
371        }
372    }
373
374    #[test]
375    fn deterministic() {
376        let mut g = WeightedGraph::new(5);
377        g.add_undirected_edge(0, 1, 1.0).expect("ok");
378        g.add_undirected_edge(1, 2, 2.0).expect("ok");
379        g.add_undirected_edge(2, 3, 1.5).expect("ok");
380        g.add_undirected_edge(3, 4, 0.5).expect("ok");
381        let a = delta_stepping(&g, 0, 1.0).expect("ok");
382        let b = delta_stepping(&g, 0, 1.0).expect("ok");
383        assert_eq!(a.dist, b.dist);
384    }
385}