Skip to content

Commit 1d94b7a

Browse files
Optimization: iFUB algorithm for unweighted graph diameter (#482)
Co-authored-by: Stefan Krastanov <[email protected]>
1 parent e7e2e43 commit 1d94b7a

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Graphs"
22
uuid = "86223c79-3864-5bf0-83f7-82e725a168b6"
3-
version = "1.13.2"
3+
version = "1.13.3"
44

55
[deps]
66
ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d"

src/distance.jl

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ end
9595
Given a graph and optional distance matrix, or a vector of precomputed
9696
eccentricities, return the maximum eccentricity of the graph.
9797
98+
An optimizied BFS algorithm (iFUB) is used for unweighted graphs, both in [undirected](https://www.sciencedirect.com/science/article/pii/S0304397512008687)
99+
and [directed](https://link.springer.com/chapter/10.1007/978-3-642-30850-5_10) cases.
100+
98101
# Examples
99102
```jldoctest
100103
julia> using Graphs
@@ -106,11 +109,127 @@ julia> diameter(path_graph(5))
106109
4
107110
```
108111
"""
112+
function diameter end
113+
109114
diameter(eccentricities::Vector) = maximum(eccentricities)
110-
function diameter(g::AbstractGraph, distmx::AbstractMatrix=weights(g))
115+
116+
diameter(g::AbstractGraph) = diameter(g, weights(g))
117+
118+
function diameter(g::AbstractGraph, ::DefaultDistance)
119+
if nv(g) == 0
120+
return 0
121+
end
122+
123+
connected = is_directed(g) ? is_strongly_connected(g) : is_connected(g)
124+
125+
if !connected
126+
return typemax(Int)
127+
end
128+
129+
return _diameter_ifub(g)
130+
end
131+
132+
function diameter(g::AbstractGraph, distmx::AbstractMatrix)
111133
return maximum(eccentricity(g, distmx))
112134
end
113135

136+
function _diameter_ifub(g::AbstractGraph{T}) where {T<:Integer}
137+
nvg = nv(g)
138+
out_list = [outneighbors(g, v) for v in vertices(g)]
139+
140+
if is_directed(g)
141+
in_list = [inneighbors(g, v) for v in vertices(g)]
142+
else
143+
in_list = out_list
144+
end
145+
146+
active = trues(nvg)
147+
visited = falses(nvg)
148+
queue = Vector{T}(undef, nvg)
149+
distbuf = fill(typemax(T), nvg)
150+
diam = 0
151+
152+
# Sort vertices by total degree (descending) to maximize pruning potential
153+
vs = collect(vertices(g))
154+
sort!(vs; by=v -> -(length(out_list[v]) + length(in_list[v])))
155+
156+
for u in vs
157+
if !active[u]
158+
continue
159+
end
160+
161+
# Forward BFS from u
162+
fill!(visited, false)
163+
visited[u] = true
164+
queue[1] = u
165+
front = 1
166+
back = 2
167+
level_end = 1
168+
e = 0
169+
170+
while front < back
171+
v = queue[front]
172+
front += 1
173+
174+
@inbounds for w in out_list[v]
175+
if !visited[w]
176+
visited[w] = true
177+
queue[back] = w
178+
back += 1
179+
end
180+
end
181+
182+
if front > level_end && front < back
183+
e += 1
184+
level_end = back - 1
185+
end
186+
end
187+
diam = max(diam, e)
188+
189+
# Backward BFS (Pruning)
190+
dmax = diam - e
191+
192+
# Only prune if we have a chance to exceed the current diameter
193+
if dmax >= 0
194+
fill!(distbuf, typemax(T))
195+
distbuf[u] = 0
196+
queue[1] = u
197+
front = 1
198+
back = 2
199+
200+
while front < back
201+
v = queue[front]
202+
front += 1
203+
204+
if distbuf[v] >= dmax
205+
continue
206+
end
207+
208+
@inbounds for w in in_list[v]
209+
if distbuf[w] == typemax(T)
210+
distbuf[w] = distbuf[v] + 1
211+
queue[back] = w
212+
back += 1
213+
end
214+
end
215+
end
216+
217+
# Prune vertices that cannot lead to a longer diameter
218+
@inbounds for v in vertices(g)
219+
if active[v] && distbuf[v] != typemax(T) && (distbuf[v] + e <= diam)
220+
active[v] = false
221+
end
222+
end
223+
end
224+
225+
if !any(active)
226+
break
227+
end
228+
end
229+
230+
return diam
231+
end
232+
114233
"""
115234
periphery(eccentricities)
116235
periphery(g, distmx=weights(g))

test/distance.jl

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
g4 = path_digraph(5)
33
adjmx1 = [0 1 0; 1 0 1; 0 1 0] # graph
44
adjmx2 = [0 1 0; 1 0 1; 1 1 0] # digraph
5+
adjmx3 = [0 1 0; 0 0 0; 0 0 0]
56
a1 = SimpleGraph(adjmx1)
67
a2 = SimpleDiGraph(adjmx2)
8+
a3 = SimpleDiGraph(adjmx3)
9+
a4 = blockdiag(complete_graph(5), complete_graph(5));
10+
add_edge!(a4, 1, 6)
711
distmx1 = [Inf 2.0 Inf; 2.0 Inf 4.2; Inf 4.2 Inf]
812
distmx2 = [Inf 2.0 Inf; 3.2 Inf 4.2; 5.5 6.1 Inf]
913

@@ -44,6 +48,74 @@
4448
@test @inferred(center(z)) == center(g, distmx2) == [2]
4549
end
4650
end
51+
52+
@testset "Disconnected graph diameter" for g in test_generic_graphs(a3)
53+
@test @inferred(diameter(g)) == typemax(Int)
54+
end
55+
56+
@testset "simplegraph diameter" for g in test_generic_graphs(a4)
57+
@test @inferred(diameter(g)) == 3
58+
end
59+
60+
@testset "Empty graph diameter" begin
61+
@test @inferred(diameter(SimpleGraph(0))) == 0
62+
@test @inferred(diameter(SimpleDiGraph(0))) == 0
63+
end
64+
65+
@testset "iFUB diameter" begin
66+
# 1. Comparing against large graphs with known diameters
67+
n_large = 5000
68+
g_path = path_graph(n_large)
69+
@test diameter(g_path) == n_large - 1
70+
71+
g_cycle = cycle_graph(n_large)
72+
@test diameter(g_cycle) == floor(Int, n_large / 2)
73+
74+
g_star = star_graph(n_large)
75+
@test diameter(g_star) == 2
76+
77+
# 2. Comparing against the original implementation for random graphs
78+
function diameter_naive(g)
79+
return maximum(eccentricity(g))
80+
end
81+
82+
NUM_SAMPLES = 50 # Adjust this to change test duration
83+
84+
for i in 1:NUM_SAMPLES
85+
# Random unweighted Graphs
86+
n = rand(10:1000) # Small to Medium size graphs
87+
p = rand() * 0.1 + 0.005 # Sparse to medium density
88+
89+
# Undirected Graphs
90+
g = erdos_renyi(n, p)
91+
@test diameter(g) == diameter_naive(g)
92+
93+
ccs = connected_components(g)
94+
largest_component = ccs[argmax(length.(ccs))]
95+
g_lscc, _ = induced_subgraph(g, largest_component)
96+
97+
if nv(g_lscc) > 1
98+
d_new = @inferred diameter(g_lscc)
99+
d_ref = diameter_naive(g_lscc)
100+
@test d_new == d_ref
101+
end
102+
103+
# Directed Graphs
104+
g_dir = erdos_renyi(n, p, is_directed=true)
105+
@test diameter(g_dir) == diameter_naive(g_dir)
106+
107+
sccs = strongly_connected_components(g_dir)
108+
largest_component_directed = sccs[argmax(length.(sccs))]
109+
g_dir_lscc, _ = induced_subgraph(g_dir, largest_component_directed)
110+
111+
if nv(g_dir_lscc) > 1
112+
d_new_dir = @inferred diameter(g_dir_lscc)
113+
d_ref_dir = diameter_naive(g_dir_lscc)
114+
@test d_new_dir == d_ref_dir
115+
end
116+
end
117+
end
118+
47119
@testset "DefaultDistance" begin
48120
@test size(Graphs.DefaultDistance()) == (typemax(Int), typemax(Int))
49121
d = @inferred(Graphs.DefaultDistance(3))

0 commit comments

Comments
 (0)