diff --git a/src/distance.jl b/src/distance.jl index df2a0d3f..4f424aa7 100644 --- a/src/distance.jl +++ b/src/distance.jl @@ -91,10 +91,14 @@ end """ diameter(eccentricities) diameter(g, distmx=weights(g)) + diameter(g::Union{SimpleGraph, SimpleDiGraph}) Given a graph and optional distance matrix, or a vector of precomputed eccentricities, return the maximum eccentricity of the graph. +For unweighted `SimpleGraph` and `SimpleDiGraph`, an optimized BFS algorithm +(iFUB) is used to avoid computing eccentricities for all vertices. + # Examples ```jldoctest julia> using Graphs @@ -107,10 +111,117 @@ julia> diameter(path_graph(5)) ``` """ diameter(eccentricities::Vector) = maximum(eccentricities) + function diameter(g::AbstractGraph, distmx::AbstractMatrix=weights(g)) return maximum(eccentricity(g, distmx)) end +function diameter(g::Union{SimpleGraph,SimpleDiGraph}) + if nv(g) <= 1 + return 0 + end + return _diameter_ifub(g) +end + +function _diameter_ifub(g::AbstractGraph{T}) where {T<:Integer} + nvg = nv(g) + out_list = [outneighbors(g, v) for v in vertices(g)] + + if is_directed(g) + in_list = [inneighbors(g, v) for v in vertices(g)] + else + in_list = out_list + end + + # Data structures + active = trues(nvg) + visited = falses(nvg) + queue = Vector{T}(undef, nvg) + distbuf = fill(typemax(T), nvg) + diam = 0 + + # Sort vertices by total degree (descending) to maximize pruning potential + vs = collect(vertices(g)) + sort!(vs; by=v -> -(length(out_list[v]) + length(in_list[v]))) + + for u in vs + if !active[u] + continue + end + + # --- Forward BFS from u --- + fill!(visited, false) + visited[u] = true + queue[1] = u + front = 1 + back = 2 + level_end = 1 + e = 0 + + while front < back + v = queue[front] + front += 1 + + @inbounds for w in out_list[v] + if !visited[w] + visited[w] = true + queue[back] = w + back += 1 + end + end + + if front > level_end && front < back + e += 1 + level_end = back - 1 + end + end + diam = max(diam, e) + + # --- Backward BFS (Pruning) --- + dmax = diam - e + + # Only prune if we have a chance to exceed the current diameter + if dmax >= 0 + fill!(distbuf, typemax(T)) + distbuf[u] = 0 + queue[1] = u + front = 1 + back = 2 + + while front < back + v = queue[front] + front += 1 + + # If current distance >= dmax, we cannot close the loop to beat diam + if distbuf[v] >= dmax + continue + end + + @inbounds for w in in_list[v] + if distbuf[w] == typemax(T) + distbuf[w] = distbuf[v] + 1 + queue[back] = w + back += 1 + end + end + end + + # Prune vertices that cannot possibly be part of a diametral path > diam + @inbounds for v in vertices(g) + if active[v] && distbuf[v] != typemax(T) && (distbuf[v] + e <= diam) + active[v] = false + end + end + end + + if !any(active) + break + end + end + + return diam +end + """ periphery(eccentricities) periphery(g, distmx=weights(g)) diff --git a/test/distance.jl b/test/distance.jl index da626bf6..ee5b58ca 100644 --- a/test/distance.jl +++ b/test/distance.jl @@ -4,6 +4,8 @@ adjmx2 = [0 1 0; 1 0 1; 1 1 0] # digraph a1 = SimpleGraph(adjmx1) a2 = SimpleDiGraph(adjmx2) + a3 = blockdiag(complete_graph(5), complete_graph(5)); + add_edge!(a3, 1, 6) distmx1 = [Inf 2.0 Inf; 2.0 Inf 4.2; Inf 4.2 Inf] distmx2 = [Inf 2.0 Inf; 3.2 Inf 4.2; 5.5 6.1 Inf] @@ -44,6 +46,63 @@ @test @inferred(center(z)) == center(g, distmx2) == [2] end end + + @testset "$(typeof(g))" for g in test_generic_graphs(a3) + @test @inferred(diameter(g)) == 3 + end + + @testset "iFUB diameter" begin + + # 1. Tests comparing against large graphs with known diameters + n_large = 5000 + g_path = path_graph(n_large) + @test diameter(g_path) == n_large - 1 + + g_cycle = cycle_graph(n_large) + @test diameter(g_cycle) == floor(Int, n_large / 2) + + g_star = star_graph(n_large) + @test diameter(g_star) == 2 + + # 2. Tests comparing against the old slow implementation for random graphs + function diameter_naive(g) + return maximum(eccentricity(g)) + end + + NUM_SAMPLES = 50 # Adjust this to change test duration + + Random.seed!(42) + for i in 1:NUM_SAMPLES + # Random unweighted Graphs + n = rand(10:1000) # Small to Medium size graphs + p = rand() * 0.1 + 0.005 # Sparse to medium density + + # Undirected Graphs + g = erdos_renyi(n, p) + ccs = connected_components(g) + largest_component = ccs[argmax(length.(ccs))] + g_lscc, _ = induced_subgraph(g, largest_component) + + if nv(g_lscc) > 1 + d_new = @inferred diameter(g_lscc) + d_ref = diameter_naive(g_lscc) + @test d_new == d_ref + end + + # Directed Graphs + g_dir = erdos_renyi(n, p, is_directed=true) + sccs = strongly_connected_components(g_dir) + largest_component_directed = sccs[argmax(length.(sccs))] + g_dir_lscc, _ = induced_subgraph(g_dir, largest_component_directed) + + if nv(g_dir_lscc) > 1 + d_new_dir = @inferred diameter(g_dir_lscc) + d_ref_dir = diameter_naive(g_dir_lscc) + @test d_new_dir == d_ref_dir + end + end + end + @testset "DefaultDistance" begin @test size(Graphs.DefaultDistance()) == (typemax(Int), typemax(Int)) d = @inferred(Graphs.DefaultDistance(3)) diff --git a/test/formattttt.jl b/test/formattttt.jl new file mode 100644 index 00000000..70e80e77 --- /dev/null +++ b/test/formattttt.jl @@ -0,0 +1,4 @@ +using JuliaFormatter + +format_file("src/distance.jl") +format_file("test/distance.jl") \ No newline at end of file