Skip to content

feat: add BMSSP single-source shortest paths - #788

Open
tachsin wants to merge 2 commits into
evenfurther:mainfrom
tachsin:feat/sssp
Open

tachsin wants to merge 2 commits into
evenfurther:mainfrom
tachsin:feat/sssp

Conversation

@tachsin

@tachsin tachsin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the SSSP algorithm from Duan, Mao, Mao, Shu, Yin 2025 for #730.

The paper's BMSSP recursion (FindPivots, base-case Dijkstra, bounded multi-source calls) is wired to the same successor-function model as dijkstra / dijkstra_all:

  • sssp_all(start, successors) -> HashMap<N, (N, C)>
  • sssp(start, successors, success) -> Option<(Vec<N>, C)>

build_path works on the sssp_all map.

What this is not

  • It does not claim the paper's (O(m\log^{2/3}n)) bound. The Lemma 3.3 block queue is a BTreeMap, and the reachable implicit graph is materialized first.
  • sssp computes all distances, then picks a cheapest successful node. Use dijkstra when the successor graph is unbounded or you only need one target.

A linear repair pass at the end fixes nodes left stale when many paths share a length. The paper assumes unique path lengths; unit-cost grids do not.

Test plan

  • cargo test --test sssp
  • Costs match dijkstra_all on the small tree, random graphs, and a grid
  • sssp path to a goal matches dijkstra on a finite graph

Closes #730

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Zero-cost edges can cause the recursive base case to loop indefinitely.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds BMSSP-based single-source shortest-path APIs alongside existing directed graph algorithms.

Changes:

  • Implements sssp and sssp_all.
  • Exports the APIs through the directed module and prelude.
  • Adds comparisons against Dijkstra across several graph types.
File summaries
File Description
src/directed/sssp.rs Implements BMSSP and path reconstruction support.
src/directed/mod.rs Registers the SSSP module.
src/lib.rs Documents and exports SSSP APIs.
tests/sssp.rs Adds correctness tests against Dijkstra.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/directed/sssp.rs Outdated
Comment on lines +332 to +336
let b_prime = seen.iter().filter_map(|&v| self.dist[v]).max();
let u: Vec<usize> = seen
.into_iter()
.filter(|&v| self.dist[v].is_some_and(|dv| less_than(&dv, b_prime)))
.collect();
Comment thread src/directed/sssp.rs Outdated
Comment on lines +8 to +9
//! A linear repair pass fixes nodes left stale when many paths share a length
//! (the paper assumes unique path lengths).
tachsin and others added 2 commits September 11, 2026 20:33
Adds sssp / sssp_all following Duan et al. (arXiv:2504.17033), with
the same successor-function API as Dijkstra. Distances match Dijkstra
on finite reachable graphs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Zero is a valid non-negative cost, but three separate things went wrong on
graphs that use it, and each of them hung rather than returning a wrong answer.

The base case stopped once `k + 1` vertices were settled and took the largest of
their distances as the new boundary, keeping only what lay strictly below it.
The paper can do that because it assumes every shortest path length is distinct;
with ties, and a zero-weight edge makes ties immediately, every settled vertex
can sit exactly on the boundary, so the caller was handed an empty set, made no
progress, and re-queued the same source for ever. It now settles until the next
vertex is strictly further away than everything already settled. That distance
is a sound boundary, everything returned lies below it, and the set is never
empty.

A zero-weight self-loop offered a vertex the distance it already had. The
tie-break on equal cost prefers the lower-numbered parent, so a vertex whose
parent was numbered above it adopted itself, and walking the parents back from
it never terminated — `sssp` would exhaust memory rather than return. A
self-loop cannot be part of a shortest path when weights are non-negative, so it
is now refused outright.

Relaxing an edge into a vertex the level had already completed put it back in
the queue at the distance it already had, to be pulled and completed again. The
same applied to sources handed back after a recursive call. Neither is re-queued
now.

All three are needed: leaving any one of them out still hangs on random graphs
with zero-cost edges.

Checked against `dijkstra_all` and `dijkstra` over 1340 random multigraphs with
zero-cost edges, self-loops and parallel edges, up to 200 nodes. The BMSSP
recursion still does its own work rather than leaning on the repair pass: with
that pass disabled, this leaves 2 of 600 graphs stale, where the previous code
left 5.

Also corrects the description of the repair pass. It is label-correcting and
re-enqueues a vertex whenever its distance improves, so it is not a single
sweep and its worst case is that of Bellman-Ford; it was described as linear.
@tachsin

tachsin commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Both review comments are addressed, and the branch is rebased onto current main.

Zero-cost edges

The report was right, and there turned out to be three separate causes. Each one hung rather than returning a wrong answer, and all three have to be fixed — leaving any one out still hangs on random graphs with zero-cost edges, which I checked by building each pair and running them.

  1. The base case could return nothing. It stopped once k + 1 vertices were settled and took the largest of their distances as the boundary, keeping only what lay strictly below. The paper may do that because it assumes distinct shortest path lengths; a zero-weight edge produces ties immediately, every settled vertex then sits exactly on the boundary, the caller receives an empty set, makes no progress and re-queues the same source for ever. It now settles until the next vertex is strictly further than everything settled so far. That distance is a sound boundary, everything returned lies below it, and the set is never empty.

  2. A zero-weight self-loop made a vertex its own parent. It offered the vertex the distance it already had, and the tie-break on equal cost prefers the lower-numbered parent, so a vertex whose parent was numbered above it adopted itself. Walking the parents back then never terminated — sssp exhausted memory rather than returning. This is why the failure looked intermittent: a self-loop on a vertex whose parent was lower-numbered was harmless. A self-loop cannot lie on a shortest path with non-negative weights, so it is now refused outright.

  3. Completed vertices were re-queued. Relaxing an edge into a vertex the level had already completed put it back in the queue at the distance it already had, to be pulled and completed again; the same applied to sources handed back after a recursive call.

Regression tests are in tests/sssp.rs: the exact graph from the review, self-loops in both parent orderings, an all-zero-cost graph where every path ties, and 200 random multigraphs with zero costs, self-loops and parallel edges checked against dijkstra_all and dijkstra. Separately I ran 1340 random multigraphs up to 200 nodes — all agree.

One thing worth stating, since it is the check I would want to see: the fixes do not simply push the work onto the repair pass. With that pass disabled, this leaves 2 of 600 random graphs stale, where the previous code left 5. An earlier attempt of mine did make the recursion lean on repair, and the comparison is what caught it.

The complexity claim

Corrected. The pass is label-correcting and re-enqueues a vertex whenever its distance improves, so it is not a single sweep and the worst case is that of Bellman-Ford. Both the module documentation and the function now say so and claim no linear bound.

Still worth your judgement

Two things the review did not raise but which I think matter more than either fix, and which I would rather say plainly than leave for you to find:

  • It is slower than dijkstra_all. On sparse random digraphs of 2k to 100k nodes I measured 1.03x to 1.23x, and 1.44x on unit-cost grids, for identical results. The paper's bound is not claimed here, so at present this buys correctness-equal output at a cost.
  • sssp computes every distance and then picks a goal, so it cannot stop early and needs the reachable graph to be finite, where dijkstra handles unbounded implicit graphs.

If that means it does not earn its place, closing this is a reasonable outcome and I would not argue. The zero-cost fixes stand on their own either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementing the SSSP algorithm

2 participants