diff --git a/lib/OptimalBranchingCore/src/OptimalBranchingCore.jl b/lib/OptimalBranchingCore/src/OptimalBranchingCore.jl index 187cc49..bedb53a 100644 --- a/lib/OptimalBranchingCore/src/OptimalBranchingCore.jl +++ b/lib/OptimalBranchingCore/src/OptimalBranchingCore.jl @@ -7,7 +7,7 @@ using DataStructures # logic expressions export Clause, BranchingTable, DNF, booleans, ∨, ∧, ¬, covered_by, literals, is_true_literal, is_false_literal # weighted minimum set cover solvers and optimal branching rule -export weighted_minimum_set_cover, AbstractSetCoverSolver, LPSolver, IPSolver +export weighted_minimum_set_cover, weighted_minimum_signed_exact_cover, AbstractSetCoverSolver, LPSolver, IPSolver export minimize_γ, optimal_branching_rule, OptimalBranchingResult ##### interfaces ##### diff --git a/lib/OptimalBranchingCore/src/bitbasis.jl b/lib/OptimalBranchingCore/src/bitbasis.jl index 5208e30..bc05efd 100644 --- a/lib/OptimalBranchingCore/src/bitbasis.jl +++ b/lib/OptimalBranchingCore/src/bitbasis.jl @@ -31,6 +31,7 @@ struct Clause{INT <: Integer} new{INT}(mask, val & mask) end end +Base.length(c::Clause) = count_ones(c.mask) function clause_string(c::Clause{INT}) where INT join([iszero(readbit(c.val, i)) ? "¬#$i" : "#$i" for i = 1:bsizeof(INT) if readbit(c.mask, i) == 1], " ∧ ") diff --git a/lib/OptimalBranchingCore/src/setcovering.jl b/lib/OptimalBranchingCore/src/setcovering.jl index 0d5eab5..3416329 100644 --- a/lib/OptimalBranchingCore/src/setcovering.jl +++ b/lib/OptimalBranchingCore/src/setcovering.jl @@ -276,54 +276,35 @@ Solves the weighted minimum set cover problem. ### Returns A vector of indices of selected subsets. """ -function weighted_minimum_set_cover(solver::LPSolver, weights::AbstractVector, subsets::Vector{Vector{Int}}, num_items::Int) +function weighted_minimum_set_cover(solver::Union{LPSolver, IPSolver}, weights::AbstractVector, subsets::Vector{Vector{Int}}, num_items::Int) nsc = length(subsets) - - sets_id = [Vector{Int}() for _=1:num_items] - for i in 1:nsc - for j in subsets[i] - push!(sets_id[j], i) - end - end + sets_id = _init_set_id(subsets, num_items) # LP by JuMP model = Model(solver.optimizer) !solver.verbose && set_silent(model) - @variable(model, 0 <= x[i = 1:nsc] <= 1) + if solver isa LPSolver + @variable(model, 0 <= x[i = 1:nsc] <= 1) + elseif solver isa IPSolver + @variable(model, 0 <= x[i = 1:nsc] <= 1, Int) + end @objective(model, Min, sum(x[i] * weights[i] for i in 1:nsc)) for i in 1:num_items @constraint(model, sum(x[j] for j in sets_id[i]) >= 1) end optimize!(model) - xs = value.(x) - @assert is_solved_by(xs, sets_id, num_items) - return pick_sets(xs, subsets, num_items) + @assert is_solved_and_feasible(model) + return pick_sets(value.(x), subsets, num_items) end - -function weighted_minimum_set_cover(solver::IPSolver, weights::AbstractVector, subsets::Vector{Vector{Int}}, num_items::Int) - nsc = length(subsets) - +function _init_set_id(subsets::Vector{Vector{Int}}, num_items::Int) sets_id = [Vector{Int}() for _=1:num_items] - for i in 1:nsc + for i in 1:length(subsets) for j in subsets[i] push!(sets_id[j], i) end end - - # IP by JuMP - model = Model(solver.optimizer) - !solver.verbose && set_silent(model) - - @variable(model, 0 <= x[i = 1:nsc] <= 1, Int) - @objective(model, Min, sum(x[i] * weights[i] for i in 1:nsc)) - for i in 1:num_items - @constraint(model, sum(x[j] for j in sets_id[i]) >= 1) - end - - optimize!(model) - @assert is_solved_and_feasible(model) - return pick_sets(value.(x), subsets, num_items) + return sets_id end # by viewing xs as the probability of being selected, we can use a random algorithm to pick the sets @@ -347,3 +328,47 @@ function pick_sets(xs::Vector, subsets::Vector{Vector{Int}}, num_items::Int) return [i for i in picked] end + +""" + weighted_minimum_signed_exact_cover(solver, weights::AbstractVector, subsets::Vector{Vector{Int}}, num_items::Int, cmax::Float64) + +Solves the weighted minimum signed exact cover problem. It is different from the unbalanced version in that the variables are now changed to a real variable rather than a binary variable. +It represents how many times a subset is selected. This number can be positive, zero, or negative. The total number of times a subset is selected must be equal to one. + +### Arguments +- `solver`: The solver to be used. It can be an instance of `LPSolver` or `IPSolver`. +- `weights::AbstractVector`: The weights of the subsets. +- `subsets::Vector{Vector{Int}}`: A vector of subsets. +- `num_items::Int`: The number of elements to cover. +- `cmax::Float64`: The maximum coefficient of the subsets. + +### Returns +A vector of weights for each subset. +""" +function weighted_minimum_signed_exact_cover(solver::Union{LPSolver, IPSolver}, weights::AbstractVector, subsets::Vector{Vector{Int}}, num_items::Int, cmax::Float64) + nsc = length(subsets) + sets_id = _init_set_id(subsets, num_items) + + # IP by JuMP + model = Model(solver.optimizer) + !solver.verbose && set_silent(model) + if solver isa LPSolver + @variable(model, 0 <= x[i = 1:nsc] <= 1) + elseif solver isa IPSolver + @variable(model, 0 <= x[i = 1:nsc] <= 1, Int) + end + @variable(model, c[i = 1:nsc]) # coefficient of the i-th subset + @objective(model, Min, sum(x[i] * weights[i] for i in 1:nsc)) + for i in 1:num_items # cover all items exactly once + @constraint(model, sum(c[j] for j in sets_id[i]) == 1) + end + for i in 1:nsc + # inspired by: https://ieeexplore.ieee.org/document/6638790 + @constraint(model, c[i] >= -cmax * x[i]) + @constraint(model, c[i] <= cmax * x[i]) + end + + optimize!(model) + @assert is_solved_and_feasible(model) + return value.(c) +end \ No newline at end of file diff --git a/lib/OptimalBranchingCore/test/bitbasis.jl b/lib/OptimalBranchingCore/test/bitbasis.jl index 7f024cf..c0602bb 100644 --- a/lib/OptimalBranchingCore/test/bitbasis.jl +++ b/lib/OptimalBranchingCore/test/bitbasis.jl @@ -11,6 +11,10 @@ using Test @test c1 == c2 @test c1 !== c3 @test c1 !== c4 + @test length(c1) == 3 + @test length(c2) == 3 + @test length(c3) == 3 + @test length(c4) == 2 # literals lts1 = literals(c1) diff --git a/lib/OptimalBranchingCore/test/setcovering.jl b/lib/OptimalBranchingCore/test/setcovering.jl index 12777ce..6d8e266 100644 --- a/lib/OptimalBranchingCore/test/setcovering.jl +++ b/lib/OptimalBranchingCore/test/setcovering.jl @@ -85,3 +85,17 @@ end @test OptimalBranchingCore.covered_by(tbl, result_ip.optimal_rule) @test result_ip.γ ≈ 1.0 end + +@testset "weighted minimum signed exact cover" begin + subsets = [[1], [2], [3], [4], [1, 2], [2, 3], [3, 4], [4, 5]] + weights = collect(1:8.0) + num_items = 5 + result_ip = OptimalBranchingCore.weighted_minimum_signed_exact_cover(IPSolver(max_itr = 10, verbose = false), weights, subsets, num_items, 10.0) + @test result_ip ≈ [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] + + subsets = [[1, 2], [2], [2,3,4,5], [4], [2, 3], [3, 4], [4, 5]] + weights = collect(1:7.0) + num_items = 5 + result_ip = OptimalBranchingCore.weighted_minimum_signed_exact_cover(IPSolver(max_itr = 10, verbose = false), weights, subsets, num_items, 10.0) + @test result_ip ≈ [1.0, -1.0, 1.0, 0.0, 0.0, 0.0, 0.0] +end \ No newline at end of file