From e7bbe14becea1f8c0db163c239605af3b8a9bf3d Mon Sep 17 00:00:00 2001 From: vera-rubin <322284620+vera-rubin@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:49:14 +0000 Subject: [PATCH] Fix SPFA negative cycle detection across all vertices --- CHANGELOG.md | 3 +++ src/shortestpaths/spfa.jl | 16 +++++++++++----- test/shortestpaths/spfa.jl | 10 ++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f99226ba..cb619ecbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ We follow SemVer as most of the Julia ecosystem. Below you might see the "breaking" label even for minor version bumps -- we use it a bit more loosely to denote things that are not breaking by SemVer's definition but might cause breakage to people using internal or experimental APIs or undocumented implementation details. +## Unreleased +- `has_negative_edge_cycle_spfa` now detects negative cycles unreachable from vertex 1 + ## v1.15.0 - 2026-09-07 - `is_articulation(g, v)` for checking whether a single vertex is an articulation point - The iFUB algorithm is used for faster diameter calculation and now supports weighted graph diameter calculation diff --git a/src/shortestpaths/spfa.jl b/src/shortestpaths/spfa.jl index e295005f0..8cec9360a 100644 --- a/src/shortestpaths/spfa.jl +++ b/src/shortestpaths/spfa.jl @@ -51,15 +51,21 @@ function spfa_shortest_paths( (source in 1:nvg) || throw(DomainError(source, "source should be in between 1 and $nvg")) + return _spfa_shortest_paths(graph, [source], distmx) +end + +function _spfa_shortest_paths( + graph::AbstractGraph{U}, sources::AbstractVector{<:Integer}, distmx::AbstractMatrix{T} +) where {T<:Number} where {U<:Integer} + nvg = nv(graph) dists = fill(typemax(T), nvg) - dists[source] = 0 + dists[sources] .= zero(T) count = zeros(U, nvg) # Vector to store the count of number of times a vertex goes in the queue. - queue = Vector{U}() # Vector used to implement queue + queue = Vector{U}(sources) # Vector used to implement queue inqueue = falses(nvg, 1) # BitArray to mark which vertices are in queue - push!(queue, source) - inqueue[source] = true + inqueue[sources] .= true @inbounds while !isempty(queue) v = popfirst!(queue) @@ -117,7 +123,7 @@ function has_negative_edge_cycle_spfa( g::AbstractGraph{U}, distmx::AbstractMatrix{T} ) where {T<:Number} where {U<:Integer} try - spfa_shortest_paths(g, 1, distmx) + _spfa_shortest_paths(g, collect_if_not_vector(vertices(g)), distmx) catch e isa(e, NegativeCycleError) && return true end diff --git a/test/shortestpaths/spfa.jl b/test/shortestpaths/spfa.jl index e6cee66df..4f062eb9f 100644 --- a/test/shortestpaths/spfa.jl +++ b/test/shortestpaths/spfa.jl @@ -197,6 +197,16 @@ end @testset "Negative Cycle" begin + @testset "Negative cycle unreachable from vertex 1" begin + gx = SimpleDiGraph(3) + add_edge!(gx, 2, 3) + add_edge!(gx, 3, 2) + d = [0 0 0; 0 0 -2; 0 1 0] + for g in test_generic_graphs(gx) + @test has_negative_edge_cycle_spfa(g, d) + end + end + # Negative Cycle 1 gx = complete_graph(3) for g in test_generic_graphs(gx)