diff --git a/docs/src/algorithms.md b/docs/src/algorithms.md index aed8f28..7131a94 100644 --- a/docs/src/algorithms.md +++ b/docs/src/algorithms.md @@ -70,6 +70,15 @@ GraphsOptim.min_vertex_cover! Finds a subset $S \subset V$ of vertices of an undirected graph $G = (V,E)$ such that $\forall (u,v) \in E: u \in S \lor v \in S$ +## Maximum weight clique + +```@docs +maximum_weight_clique +GraphsOptim.maximum_weight_clique! +``` + +A *clique* is a subset $S \subset V$ of vertices of an undirected graph $G = (V,E)$ such that $\forall (u,v) \in S: (u, v) \in E$. We search for the clique maximizing the total weight of selected vertices. + ## Maximum Weight Independent Set ```@docs diff --git a/src/GraphsOptim.jl b/src/GraphsOptim.jl index ec187fd..7156d42 100644 --- a/src/GraphsOptim.jl +++ b/src/GraphsOptim.jl @@ -27,6 +27,7 @@ export min_vertex_cover export maximum_weight_independent_set export fractional_chromatic_number, fractional_clique_number export shortest_path +export maximum_weight_clique include("utils.jl") include("flow.jl") @@ -35,6 +36,7 @@ include("graph_matching.jl") include("min_vertex_cover.jl") include("fractional_coloring.jl") include("shortest_path.jl") +include("maximum_clique.jl") include("independent_set.jl") end diff --git a/src/maximum_clique.jl b/src/maximum_clique.jl new file mode 100644 index 0000000..e0aad86 --- /dev/null +++ b/src/maximum_clique.jl @@ -0,0 +1,49 @@ + +""" + maximum_weight_clique!(model, g; var_name) + +Computes in-place in the JuMP model a maximum-weighted clique of `g`. +An optional `vertex_weights` vector can be passed to the graph, defaulting to uniform weights (computing a maximum size clique). +""" +function maximum_weight_clique!( + model::Model, g::AbstractGraph; binary::Bool=true, var_name, vertex_weights=ones(nv(g)) +) + if is_directed(g) + throw(ArgumentError("The graph must not be directed")) + end + g_vertices = collect(vertices(g)) + f = @variable(model, [g_vertices]; binary=binary, base_name=String(var_name)) + model[Symbol(var_name)] = f + @constraint( + model, + packing_constraint[i=1:nv(g), j=1:nv(g); i ≠ j && !has_edge(g, i, j)], + f[i] + f[j] <= 1, + ) + obj = objective_function(model) + add_to_expression!(obj, dot(f, vertex_weights)) + @objective(model, Max, obj) + return model +end + +""" + maximum_weight_clique(g; optimizer, binary, vertex_weights) + +Computes a maximum-weighted clique of `g`. +""" +function maximum_weight_clique( + g::AbstractGraph; + binary::Bool=true, + vertex_weights=ones(nv(g)), + optimizer=HiGHS.Optimizer, +) + model = Model(optimizer) + set_silent(model) + maximum_weight_clique!( + model, g; binary=binary, vertex_weights=vertex_weights, var_name=:clique + ) + optimize!(model) + @assert termination_status(model) == OPTIMAL + clique_variables = Vector(model[:clique]) + clique_vertices = findall(v -> value(v) > 0.5, clique_variables) + return clique_vertices +end diff --git a/test/maximum_clique.jl b/test/maximum_clique.jl new file mode 100644 index 0000000..fd64226 --- /dev/null +++ b/test/maximum_clique.jl @@ -0,0 +1,21 @@ +using GraphsOptim +using Graphs +using Test + +g = Graphs.random_regular_graph(10, 5) + +for _ in 1:10 + vertex_weights = rand(nv(g)) + clique = GraphsOptim.maximum_weight_clique(g; vertex_weights=vertex_weights) + if length(clique) > 1 + for idx in 1:(length(clique) - 1) + @test Graphs.has_edge(g, clique[idx], clique[idx + 1]) + end + end +end + +g2 = complete_graph(3) +add_vertex!(g2) +add_edge!(g2, 3, 4) +clique = GraphsOptim.maximum_weight_clique(g2) +@test sort(clique) == 1:3 diff --git a/test/runtests.jl b/test/runtests.jl index 4773df1..fb245da 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,6 +44,10 @@ using Test include("min_vertex_cover.jl") end + @testset verbose = true "Cliques" begin + include("maximum_clique.jl") + end + @testset verbose = true "Independent set" begin include("independent_set.jl") end