Skip to content

Commit e8f3a49

Browse files
authored
Mincut (#105)
* fixes mincut. fixes #64 * add check for non-negative edge weight * decrease recording of solutions * fix for directed graphs * disable for directed graphs * Revert "fix for directed graphs" This reverts commit 73104f8. * disable directed graphs * fix disable directed graphs * new heuristic; fixed many things * remove comment * fix test
1 parent 1cb789d commit e8f3a49

File tree

2 files changed

+123
-39
lines changed

2 files changed

+123
-39
lines changed

src/traversals/maxadjvisit.jl

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,60 +12,119 @@
1212
1313
Return a tuple `(parity, bestcut)`, where `parity` is a vector of integer
1414
values that determines the partition in `g` (1 or 2) and `bestcut` is the
15-
weight of the cut that makes this partition. An optional `distmx` matrix may
16-
be specified; if omitted, edge distances are assumed to be 1.
15+
weight of the cut that makes this partition. An optional `distmx` matrix
16+
of non-negative weights may be specified; if omitted, edge distances are
17+
assumed to be 1.
1718
"""
18-
function mincut(g::AbstractGraph, distmx::AbstractMatrix{T}=weights(g)) where {T<:Real}
19-
U = eltype(g)
20-
colormap = zeros(UInt8, nv(g)) ## 0 if unseen, 1 if processing and 2 if seen and closed
21-
parities = falses(nv(g))
22-
bestweight = typemax(T)
23-
cutweight = zero(T)
24-
visited = zero(U) ## number of vertices visited
25-
pq = PriorityQueue{U,T}(Base.Order.Reverse)
19+
@traitfn function mincut(g::::(!IsDirected), distmx::AbstractMatrix{T}=weights(g)) where {T <: Real}
2620

27-
# Set number of visited neighbors for all vertices to 0
28-
for v in vertices(g)
29-
pq[v] = zero(T)
30-
end
21+
nvg = nv(g)
22+
U = eltype(g)
3123

3224
# make sure we have at least two vertices, otherwise, there's nothing to cut,
3325
# in which case we'll return immediately.
34-
(haskey(pq, one(U)) && nv(g) > one(U)) || return (Vector{Int8}([1]), cutweight)
35-
36-
# Give the starting vertex high priority
37-
pq[one(U)] = one(T)
26+
(nvg > one(U)) || return (Vector{Int8}([1]), zero(T))
3827

39-
while !isempty(pq)
40-
u = dequeue!(pq)
41-
colormap[u] = 1
28+
is_merged = falses(nvg)
29+
merged_vertices = IntDisjointSets(U(nvg))
30+
graph_size = nvg
31+
# We need to mutate the weight matrix,
32+
# and we need it clean (0 for non edges)
33+
w = zeros(T, nvg, nvg)
34+
size(distmx) != (nvg, nvg) && throw(ArgumentError("Adjacency / distance matrix size should match the number of vertices"))
35+
@inbounds for e in edges(g)
36+
d = distmx[src(e), dst(e)]
37+
(d < 0) && throw(DomainError(w, "weigths should be non-negative"))
38+
w[src(e), dst(e)] = d
39+
(d != distmx[dst(e), src(e)]) && throw(ArgumentError("Adjacency / distance matrix must be symmetric"))
40+
w[dst(e), src(e)] = d
41+
end
42+
# we also need to mutate neighbors when merging vertices
43+
fadjlist = [collect(outneighbors(g, v)) for v in vertices(g)]
44+
parities = falses(nvg)
45+
bestweight = typemax(T)
46+
pq = PriorityQueue{U,T}(Base.Order.Reverse)
47+
u = last_vertex = one(U)
4248

43-
for v in outneighbors(g, u)
44-
# if the target of e is already marked then decrease cutweight
45-
# otherwise, increase it
46-
ew = distmx[u, v]
47-
if colormap[v] != 0
48-
cutweight -= ew
49-
else
50-
cutweight += ew
49+
is_processed = falses(nvg)
50+
@inbounds while graph_size > 1
51+
cutweight = zero(T)
52+
is_processed .= false
53+
is_processed[u] = true
54+
# initialize pq
55+
for v in vertices(g)
56+
is_merged[v] && continue
57+
v == u && continue
58+
pq[v] = zero(T)
59+
end
60+
for v in fadjlist[u]
61+
(is_merged[v] || v == u ) && continue
62+
pq[v] = w[u, v]
63+
cutweight += w[u, v]
64+
end
65+
# Minimum cut phase
66+
while true
67+
last_vertex = u
68+
u, adj_cost = first(pq)
69+
dequeue!(pq)
70+
isempty(pq) && break
71+
for v in fadjlist[u]
72+
(is_merged[v] || u == v) && continue
73+
# if the target of e is already marked then decrease cutweight
74+
# otherwise, increase it
75+
ew = w[u, v]
76+
if is_processed[v]
77+
cutweight -= ew
78+
else
79+
cutweight += ew
80+
pq[v] += ew
81+
end
5182
end
52-
if haskey(pq, v)
53-
pq[v] += distmx[u, v]
83+
is_processed[u] = true
84+
# adj_cost is a lower bound on the cut separating the two last vertices
85+
# encountered, so if adj_cost >= bestweight, we can already merge these
86+
# vertices to save one phase.
87+
if adj_cost >= bestweight
88+
_merge_vertex!(merged_vertices, fadjlist, is_merged, w, u, last_vertex)
89+
graph_size -= 1
5490
end
5591
end
5692

57-
colormap[u] = 2
58-
visited += one(U)
59-
if cutweight < bestweight && visited < nv(g)
93+
# check if we improved the mincut
94+
if cutweight < bestweight
6095
bestweight = cutweight
61-
for u in vertices(g)
62-
parities[u] = (colormap[u] == 2)
96+
for v in vertices(g)
97+
parities[v] = (find_root!(merged_vertices, v) == u)
6398
end
6499
end
100+
101+
# merge u and last_vertex
102+
root = _merge_vertex!(merged_vertices, fadjlist, is_merged, w, u, last_vertex)
103+
graph_size -= 1
104+
u = root # we are sure this vertex was not merged, so the next phase start from it
65105
end
66106
return (convert(Vector{Int8}, parities) .+ one(Int8), bestweight)
67107
end
68108

109+
function _merge_vertex!(merged_vertices, fadjlist, is_merged, w, u, v)
110+
root = union!(merged_vertices, u, v)
111+
non_root = (root == u) ? v : u
112+
is_merged[non_root] = true
113+
# update weights
114+
for v2 in fadjlist[non_root]
115+
w[root, v2] += w[non_root, v2]
116+
w[v2, root] = w[root, v2]
117+
end
118+
# update neighbors
119+
fadjlist[root] = union(fadjlist[root], fadjlist[non_root])
120+
for v in fadjlist[non_root]
121+
if root fadjlist[v]
122+
push!(fadjlist[v], root)
123+
end
124+
end
125+
return root
126+
end
127+
69128
"""
70129
maximum_adjacency_visit(g[, distmx][, log][, io][, s])
71130
maximum_adjacency_visit(g[, s])
@@ -106,7 +165,7 @@ function maximum_adjacency_visit(
106165
log && println(io, "discover vertex: $u")
107166
for v in outneighbors(g, u)
108167
log && println(io, " -- examine neighbor from $u to $v")
109-
if has_key[v]
168+
if has_key[v] && (u != v)
110169
ed = distmx[u, v]
111170
pq[v] += ed
112171
end

test/traversals/maxadjvisit.jl

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@
3333
@test ne(g) == m
3434

3535
parity, bestcut = @inferred(mincut(g, eweights))
36+
if parity[1] == 2
37+
parity .= 3 .- parity
38+
end
3639

3740
@test length(parity) == 8
38-
@test parity == [2, 2, 1, 1, 2, 2, 1, 1]
41+
@test parity == [1, 1, 2, 2, 1, 1, 2, 2]
3942
@test bestcut == 4.0
4043

4144
parity, bestcut = @inferred(mincut(g))
4245

4346
@test length(parity) == 8
44-
@test parity == [2, 1, 1, 1, 1, 1, 1, 1]
4547
@test bestcut == 2.0
4648

4749
v = @inferred(maximum_adjacency_visit(g))
@@ -55,4 +57,27 @@
5557
end
5658
@test maximum_adjacency_visit(gx, 1) == [1, 2, 5, 6, 3, 7, 4, 8]
5759
@test maximum_adjacency_visit(gx, 3) == [3, 2, 7, 4, 6, 8, 5, 1]
60+
61+
# non regression test for #64
62+
g = clique_graph(4, 2)
63+
w = zeros(Int, 8, 8)
64+
for e in edges(g)
65+
if src(e) in [1, 5] || dst(e) in [1, 5]
66+
w[src(e), dst(e)] = 3
67+
else
68+
w[src(e), dst(e)] = 2
69+
end
70+
w[dst(e), src(e)] = w[src(e), dst(e)]
71+
end
72+
w[1, 5] = 6
73+
w[5, 1] = 6
74+
parity, bestcut = @inferred(mincut(g, w))
75+
if parity[1] == 2
76+
parity .= 3 .- parity
77+
end
78+
@test parity == [1, 1, 1, 1, 2, 2, 2, 2]
79+
@test bestcut == 6
80+
81+
w[6,7] = -1
82+
@test_throws DomainError mincut(g, w)
5883
end

0 commit comments

Comments
 (0)