Conversation
e98f893 to
ec515dd
Compare
There was a problem hiding this comment.
🟡 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
ssspandsssp_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.
| 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(); |
| //! A linear repair pass fixes nodes left stale when many paths share a length | ||
| //! (the paper assumes unique path lengths). |
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.
|
Both review comments are addressed, and the branch is rebased onto current Zero-cost edgesThe 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.
Regression tests are in 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 claimCorrected. 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 judgementTwo 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:
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. |
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 asdijkstra/dijkstra_all:sssp_all(start, successors) -> HashMap<N, (N, C)>sssp(start, successors, success) -> Option<(Vec<N>, C)>build_pathworks on thesssp_allmap.What this is not
BTreeMap, and the reachable implicit graph is materialized first.ssspcomputes all distances, then picks a cheapest successful node. Usedijkstrawhen 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 ssspdijkstra_allon the small tree, random graphs, and a gridsssppath to a goal matchesdijkstraon a finite graphCloses #730