Skip to content

Commit 3f908f8

Browse files
committed
Refactor tearing code a bit more
In preparation of some useful debug tooling and to generalize tearing to remove the assumption that tearing leaves the sccs invariant.
1 parent 6c1e78b commit 3f908f8

File tree

1 file changed

+90
-24
lines changed

1 file changed

+90
-24
lines changed

src/bipartite_graph.jl

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module BipartiteGraphs
22

33
export BipartiteEdge, BipartiteGraph, DiCMOBiGraph, Unassigned, unassigned,
4-
Matching
4+
Matching, ResidualCMOGraph
55

66
export 𝑠vertices, 𝑑vertices, has_𝑠vertex, has_𝑑vertex, 𝑠neighbors, 𝑑neighbors,
77
𝑠edges, 𝑑edges, nsrcs, ndsts, SRC, DST, set_neighbors!, invview,
@@ -19,51 +19,59 @@ struct Unassigned
1919
const unassigned = Unassigned.instance
2020
end
2121

22-
struct Matching{V<:AbstractVector{<:Union{Unassigned, Int}}} <: AbstractVector{Union{Unassigned, Int}}
22+
struct Matching{U #=> :Unassigned =#, V<:AbstractVector} <: AbstractVector{Union{U, Int}}
2323
match::V
2424
inv_match::Union{Nothing, V}
2525
end
26-
Matching(v::V) where {V<:AbstractVector{<:Union{Unassigned, Int}}} =
27-
Matching{V}(v, nothing)
28-
Matching(m::Int) = Matching(Union{Int, Unassigned}[unassigned for _ = 1:m], nothing)
29-
Matching(m::Matching) = m
26+
# These constructors work around https://github.com/JuliaLang/julia/issues/41948
27+
function Matching{V}(m::Matching) where {V}
28+
eltype(m) === Union{V, Int} && return M
29+
VUT = typeof(similar(m.match, Union{V, Int}))
30+
Matching{V}(convert(VUT, m.match),
31+
m.inv_match === nothing ? nothing : convert(VUT, m.inv_match))
32+
end
33+
Matching{U}(v::V) where {U, V<:AbstractVector} = Matching{U, V}(v, nothing)
34+
Matching{U}(v::V, iv::Union{V, Nothing}) where {U, V<:AbstractVector} = Matching{U, V}(v, iv)
35+
Matching(v::V) where {U, V<:AbstractVector{Union{U, Int}}} =
36+
Matching{@isdefined(U) ? U : Unassigned, V}(v, nothing)
37+
Matching(m::Int) = Matching{Unassigned}(Union{Int, Unassigned}[unassigned for _ = 1:m], nothing)
3038

3139
Base.size(m::Matching) = Base.size(m.match)
3240
Base.getindex(m::Matching, i::Integer) = m.match[i]
3341
Base.iterate(m::Matching, state...) = iterate(m.match, state...)
3442
Base.copy(m::Matching) = Matching(copy(m.match), m.inv_match === nothing ? nothing : copy(m.inv_match))
35-
function Base.setindex!(m::Matching, v::Union{Integer, Unassigned}, i::Integer)
43+
function Base.setindex!(m::Matching{U}, v::Union{Integer, U}, i::Integer) where {U}
3644
if m.inv_match !== nothing
3745
oldv = m.match[i]
38-
oldv !== unassigned && (m.inv_match[oldv] = unassigned)
39-
v !== unassigned && (m.inv_match[v] = i)
46+
isa(oldv, Int) && (m.inv_match[oldv] = unassigned)
47+
isa(v, Int) && (m.inv_match[v] = i)
4048
end
4149
return m.match[i] = v
4250
end
4351

44-
function Base.push!(m::Matching, v::Union{Integer, Unassigned})
52+
function Base.push!(m::Matching{U}, v::Union{Integer, U}) where {U}
4553
push!(m.match, v)
4654
if v !== unassigned && m.inv_match !== nothing
4755
m.inv_match[v] = length(m.match)
4856
end
4957
end
5058

51-
function complete(m::Matching)
59+
function complete(m::Matching{U}) where {U}
5260
m.inv_match !== nothing && return m
53-
inv_match = Union{Unassigned, Int}[unassigned for _ = 1:length(m.match)]
61+
inv_match = Union{U, Int}[unassigned for _ = 1:length(m.match)]
5462
for (i, eq) in enumerate(m.match)
55-
eq === unassigned && continue
63+
isa(eq, Int) || continue
5664
inv_match[eq] = i
5765
end
58-
return Matching(collect(m.match), inv_match)
66+
return Matching{U}(collect(m.match), inv_match)
5967
end
6068

6169
@noinline require_complete(m::Matching) =
6270
m.inv_match === nothing && throw(ArgumentError("Backwards matching not defined. `complete` the matching first."))
6371

64-
function invview(m::Matching)
72+
function invview(m::Matching{U, V}) where {U, V}
6573
require_complete(m)
66-
return Matching(m.inv_match, m.match)
74+
return Matching{U, V}(m.inv_match, m.match)
6775
end
6876

6977
###
@@ -355,6 +363,14 @@ The resulting graph has a few desirable properties. In particular, this graph
355363
is acyclic if and only if the induced directed graph on the original bipartite
356364
graph is acyclic.
357365
366+
# Hypergraph interpretation
367+
368+
Consider the bipartite graph `B` as the incidence graph of some hypergraph `H`.
369+
Note that a maching `M` on `B` in the above sense is equivalent to determining
370+
an (1,n)-orientation on the hypergraph (i.e. each directed hyperedge has exactly
371+
one head, but any arbitrary number of tails). In this setting, this is simply
372+
the graph formed by expanding each directed hyperedge into `n` ordinary edges
373+
between the same vertices.
358374
"""
359375
mutable struct DiCMOBiGraph{Transposed, I, G<:BipartiteGraph{I}, M <: Matching} <: Graphs.AbstractGraph{I}
360376
graph::G
@@ -385,8 +401,7 @@ struct CMONeighbors{Transposed, V}
385401
end
386402

387403
Graphs.outneighbors(g::DiCMOBiGraph{false}, v) = CMONeighbors{false}(g, v)
388-
Graphs.inneighbors(g::DiCMOBiGraph{false}, v) = CMONeighbors{true}(invview(g), v)
389-
Graphs.all_neighbors(g::DiCMOBiGraph{true}, v::Integer) = 𝑠neighbors(g.graph, v)
404+
Graphs.inneighbors(g::DiCMOBiGraph{false}, v) = inneighbors(invview(g), v)
390405
Base.iterate(c::CMONeighbors{false}) = iterate(c, (c.g.graph.fadjlist[c.v],))
391406
function Base.iterate(c::CMONeighbors{false}, (l, state...))
392407
while true
@@ -405,15 +420,15 @@ function Base.iterate(c::CMONeighbors{false}, (l, state...))
405420
end
406421
Base.length(c::CMONeighbors{false}) = count(_->true, c)
407422

408-
lift(f, x) = (x === unassigned || isnothing(x)) ? nothing : f(x)
423+
liftint(f, x) = (!isa(x, Int)) ? nothing : f(x)
424+
liftnothing(f, x) = x === nothing ? nothing : f(x)
409425

410426
_vsrc(c::CMONeighbors{true}) = c.g.matching[c.v]
411-
_neighbors(c::CMONeighbors{true}) = lift(vsrc->c.g.graph.fadjlist[vsrc], _vsrc(c))
412-
Base.length(c::CMONeighbors{true}) = something(lift(length, _neighbors(c)), 1) - 1
427+
_neighbors(c::CMONeighbors{true}) = liftint(vsrc->c.g.graph.fadjlist[vsrc], _vsrc(c))
428+
Base.length(c::CMONeighbors{true}) = something(liftnothing(length, _neighbors(c)), 1) - 1
413429
Graphs.inneighbors(g::DiCMOBiGraph{true}, v) = CMONeighbors{true}(g, v)
414-
Graphs.outneighbors(g::DiCMOBiGraph{true}, v) = CMONeighbors{false}(invview(g), v)
415-
Graphs.all_neighbors(g::DiCMOBiGraph{true}, v::Integer) = 𝑑neighbors(g.graph, v)
416-
Base.iterate(c::CMONeighbors{true}) = lift(ns->iterate(c, (ns,)), _neighbors(c))
430+
Graphs.outneighbors(g::DiCMOBiGraph{true}, v) = outneighbors(invview(g), v)
431+
Base.iterate(c::CMONeighbors{true}) = liftnothing(ns->iterate(c, (ns,)), _neighbors(c))
417432
function Base.iterate(c::CMONeighbors{true}, (l, state...))
418433
while true
419434
r = iterate(l, state...)
@@ -442,4 +457,55 @@ end
442457
Graphs.has_edge(g::DiCMOBiGraph{true}, a, b) = a in inneighbors(g, b)
443458
Graphs.has_edge(g::DiCMOBiGraph{false}, a, b) = b in outneighbors(g, a)
444459

460+
"""
461+
struct ResidualCMOGraph
462+
463+
For a bipartite graph and matching on the graph's destination vertices, this
464+
wrapper exposes the induced graph on the destination vertices formed by those
465+
destination and source vertices that are left unmatched. In particular, two
466+
(destination) vertices a and b are neighbors if they are both unassigned and
467+
there is some unassigned source vertex `s` such that `s` is a neighbor (in the
468+
bipartite graph) of both `a` and `b`.
469+
470+
# Hypergraph interpreation
471+
472+
Refer to the hypergraph interpretation of the DiCMOBiGraph. Now consider the
473+
hypergraph left over after removing all edges that are oriented by the mapping.
474+
This graph is the undirected graph obtained by replacing all hyper edges by the
475+
maximal undirected graph on the vertices that are members of the original hyper
476+
edge.
477+
478+
# Nota Bene
479+
480+
1. For technical reasons, the `vertices` function includes even those vertices
481+
vertices that are assigned in the original hypergraph, even though they
482+
are conceptually not part of the graph.
483+
2. This graph is not strict. In particular, multi edges between vertices are
484+
allowed and common.
485+
"""
486+
struct ResidualCMOGraph{I, G<:BipartiteGraph{I}, M <: Matching} <: Graphs.AbstractGraph{I}
487+
graph::G
488+
matching::M
489+
function ResidualCMOGraph{I, G, M}(g::G, m::M) where {I, G<:BipartiteGraph{I}, M}
490+
require_complete(g)
491+
require_complete(m)
492+
new{I, G, M}(g, m)
493+
end
494+
end
495+
ResidualCMOGraph(g::G, m::M) where {I, G<:BipartiteGraph{I}, M} = ResidualCMOGraph{I, G, M}(g, m)
496+
497+
invview(rcg::ResidualCMOGraph) = ResidualCMOGraph(invview(rcg.graph), invview(rcg.matching))
498+
499+
Graphs.is_directed(::Type{<:ResidualCMOGraph}) = false
500+
Graphs.nv(rcg::ResidualCMOGraph) = ndsts(rcg.graph)
501+
Graphs.vertices(rcg::ResidualCMOGraph) = 𝑑vertices(rcg.graph)
502+
function Graphs.neighbors(rcg::ResidualCMOGraph, v::Integer)
503+
rcg.matching[v] !== unassigned && return ()
504+
Iterators.filter(
505+
vdst->rcg.matching[vdst] === unassigned,
506+
Iterators.flatten(rcg.graph.fadjlist[vsrc] for
507+
vsrc in rcg.graph.badjlist[v] if
508+
invview(rcg.matching)[vsrc] === unassigned))
509+
end
510+
445511
end # module

0 commit comments

Comments
 (0)