Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -14739,6 +14739,58 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ The hitting set $H$ is directly the vertex cover: $c_v = h_v$ for each $v in V$.
]

#let hs_sc = load-example("MinimumHittingSet", "MinimumSetCovering")
#let hs_sc_sol = hs_sc.solutions.at(0)
#let hs_sc_n = hs_sc.source.instance.universe_size
#let hs_sc_sets = hs_sc.source.instance.sets
#let hs_sc_dual = hs_sc.target.instance.sets
#let hs_sc_pick = hs_sc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i)
#let hs_sc_hits = hs_sc_sets.map(s => s.filter(e => e in hs_sc_pick))
#let hs_sc_covered = hs_sc_pick.map(u => hs_sc_dual.at(u)).flatten().dedup().sorted()
#let hs_sc_feasible = range(calc.pow(2, hs_sc_n)).filter(mask => {
let pick = range(hs_sc_n).filter(u => calc.rem(calc.quo(mask, calc.pow(2, u)), 2) == 1)
hs_sc_sets.all(s => s.any(e => e in pick))
})
#let hs_sc_unit = range(hs_sc_n).filter(u => calc.pow(2, u) in hs_sc_feasible)
#let hs_sc_min = calc.min(..hs_sc_feasible.map(mask => range(hs_sc_n).filter(u => calc.rem(calc.quo(mask, calc.pow(2, u)), 2) == 1).len()))
#assert(hs_sc_dual.len() == hs_sc_n and hs_sc.target.instance.universe_size == hs_sc_sets.len())
#assert(hs_sc_sets.all(s => s.len() > 0) and hs_sc_hits.all(h => h.len() > 0))
#assert(hs_sc_covered == range(hs_sc_sets.len()) and hs_sc_pick.len() == hs_sc_min)
#assert(hs_sc_unit.len() == 0 and hs_sc_pick.len() == 2)
#let hs_sc_no_n = 3
#let hs_sc_no_sets = ((0, 1), (), (2,))
#let hs_sc_no_dual = range(hs_sc_no_n).map(u => range(hs_sc_no_sets.len()).filter(j => u in hs_sc_no_sets.at(j)))
#let hs_sc_no_picks = range(calc.pow(2, hs_sc_no_n)).map(mask => range(hs_sc_no_n).filter(u => calc.rem(calc.quo(mask, calc.pow(2, u)), 2) == 1))
#let hs_sc_no_empty = range(hs_sc_no_sets.len()).filter(j => hs_sc_no_sets.at(j).len() == 0)
#assert(hs_sc_no_empty == (1,))
#assert(hs_sc_no_picks.all(h => not hs_sc_no_sets.all(s => s.any(e => e in h))))
#assert(hs_sc_no_picks.all(h => h.map(u => hs_sc_no_dual.at(u)).flatten().dedup().sorted() != range(hs_sc_no_sets.len())))
#assert(hs_sc_no_dual.flatten().all(j => j != 1))
#assert(hs_sc_feasible.filter(mask => range(hs_sc_n).filter(u => calc.rem(calc.quo(mask, calc.pow(2, u)), 2) == 1).len() == hs_sc_min).len() == 1)
#reduction-rule("MinimumHittingSet", "MinimumSetCovering",
example: true,
example-caption: [Hitting Set with $|U| = #hs_sc_n$ elements and $m = #hs_sc_sets.len()$ sets],
extra: [
#pred-commands(
"pred create --example " + rule-spec(hs_sc) + " -o hs.json",
"pred reduce hs.json --via route.json -o bundle.json",
"pred solve bundle.json",
"pred evaluate hs.json --config " + cli-config(hs_sc_sol.source_config),
)
*YES instance.* $U = {#range(hs_sc_n).map(str).join(", ")}$ with sets #hs_sc_sets.enumerate().map(((j, s)) => $C_#j = {#fmt-values(s)}$).join(", "). The target has $|U'| = #hs_sc.target.instance.universe_size$ elements and the unit-weight sets #hs_sc_dual.enumerate().map(((u, d)) => $D_#u = {#fmt-values(d)}$).join(", "). The hitting set $H = {#fmt-values(hs_sc_pick)}$ meets #hs_sc_hits.enumerate().map(((j, h)) => $C_#j inter H = {#fmt-values(h)}$).join(", "), and the selected sets cover $D_#hs_sc_pick.at(0) union D_#hs_sc_pick.at(1) = {#fmt-values(hs_sc_covered)} = U'$, both of value #hs_sc_pick.len(). Of the $2^#hs_sc_n$ selections, #hs_sc_feasible.len() are feasible on both sides and the optimum #hs_sc_min is attained only by $H$; no singleton is feasible.

*NO instance.* $U = {#range(hs_sc_no_n).map(str).join(", ")}$ with sets #hs_sc_no_sets.enumerate().map(((j, s)) => $C_#j = {#fmt-values(s)}$).join(", ") has no hitting set, since no element meets $C_#hs_sc_no_empty.at(0) = emptyset$ (all #hs_sc_no_picks.len() selections fail). The construction gives $U' = {#range(hs_sc_no_sets.len()).map(str).join(", ")}$ and #hs_sc_no_dual.enumerate().map(((u, d)) => $D_#u = {#fmt-values(d)}$).join(", "). Target element $#hs_sc_no_empty.at(0)$ lies in no $D_u$, so none of the #hs_sc_no_picks.len() selections covers $U'$ and the target is infeasible as well.
],
)[
Hitting a family of sets and covering a universe are dual: transposing the incidence relation turns each element into the set of members containing it. This is the dual set system construction, as in the CUHK CSCI3160 exercise solution @tao2024csci3160. @garey1979 gives only the two problem definitions.
][
_Construction._ Given $(U, cal(C))$ with $U = {0, ..., n-1}$ and $cal(C) = (C_0, ..., C_(m-1))$, let the target universe be $U' = {0, ..., m-1}$, one element per source set. For each $u in U$ let $D_u = {j in U' : u in C_j}$ with weight $1$. The target has $n$ sets and $m$ elements, so `universe_size` $= m$ and `num_sets` $= n$ exactly. Empty families, empty members, repeated members and unused elements need no case split: an unused element gives an empty $D_u$, an empty member gives an element in no $D_u$.

_Correctness._ For $H subset.eq U$, $j in union.big_(u in H) D_u$ iff some $u in H$ has $u in C_j$ iff $H inter C_j != emptyset$. ($arrow.r.double$) If $H$ hits every $C_j$, every $j in U'$ is covered by the sets $D_u$, $u in H$. ($arrow.l.double$) If ${D_u : u in H}$ covers $U'$, each $C_j$ contains some $u in H$. Both sides count $|H|$ unit weights, so feasibility and value coincide for every selection; an empty member makes both sides infeasible.

_Solution extraction._ The selection vector is returned unchanged (length $n$) after the target witness is checked to be a cover. The objective sum is at most $n$ and the reduction rejects $n > 2^63 - 1$ with a typed error, so the `i64` value never overflows.
]

#reduction-rule("MinimumHittingSet", "ILP")[
Each set must contain at least one selected element -- a standard set-covering constraint on the element indicators.
][
Expand Down Expand Up @@ -17442,6 +17494,7 @@ The following table shows concrete target-variable counts for example instances,
),
(source: "MaximumMatching", target: "MaximumSetPacking"),
(source: "MinimumVertexCover", target: "MinimumSetCovering"),
(source: "MinimumHittingSet", target: "MinimumSetCovering"),
(source: "MaxCut", target: "SpinGlass"),
(source: "SpinGlass", target: "MaxCut"),
(
Expand Down
7 changes: 7 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2352,3 +2352,10 @@ @article{een2006
pages = {1--26},
doi = {10.3233/SAT190014},
}

@misc{tao2024csci3160,
author = {Yufei Tao},
title = {{CSCI3160} Design and Analysis of Algorithms, Exercise 13 Solutions},
howpublished = {Chinese University of Hong Kong, Fall 2024},
url = {https://web.archive.org/web/2024/https://www.cse.cuhk.edu.hk/~taoyf/course/3160/24-fall/ex/ex13-sol.pdf}
}
2 changes: 1 addition & 1 deletion src/models/set/minimum_set_covering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ impl<W: Clone + Default> MinimumSetCovering<W> {
.unwrap_or_else(|error| panic!("{error}"))
}

fn try_with_weights(
pub(crate) fn try_with_weights(
universe_size: usize,
sets: Vec<Vec<usize>>,
weights: Vec<W>,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/minimumhittingset_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl ReduceTo<ILP<bool>> for MinimumHittingSet {
.collect();
let objective: Vec<(usize, i64)> = (0..num_vars).map(|i| (i, 1)).collect();
let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
.map_err(Self::target_construction)?;
.map_err(<Self as ReduceTo<ILP<bool>>>::target_construction)?;
Ok(ReductionHSToILP { target })
}
}
Expand Down
102 changes: 102 additions & 0 deletions src/rules/minimumhittingset_minimumsetcovering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Reduction from MinimumHittingSet to MinimumSetCovering.
//!
//! The target is the dual set system: one target element per source set, and
//! one unit-weight target set per source element containing the indices of the
//! source sets in which that element occurs. A selection of source elements
//! hits every source set exactly when the corresponding target sets cover every
//! target element, so the selection vectors coincide.

use crate::models::set::{MinimumHittingSet, MinimumSetCovering};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};

/// Result of reducing MinimumHittingSet to MinimumSetCovering.
#[derive(Debug, Clone)]
pub struct ReductionHSToSC {
target: MinimumSetCovering<i64>,
}

impl ReductionResult for ReductionHSToSC {
type Source = MinimumHittingSet;
type Target = MinimumSetCovering<i64>;

fn target_problem(&self) -> &Self::Target {
&self.target
}

/// Solution extraction: element `u` is selected in the hitting set exactly
/// when target set `D_u` is selected in the cover.
fn extract_solution(
&self,
target_solution: &<Self::Target as crate::traits::Problem>::Solution,
) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
crate::rules::traits::validate_target_witness(
self.target_problem(),
target_solution,
|value| value.0.is_some(),
"selected sets do not cover the universe",
)?;

Ok(target_solution.to_vec())
}
}

#[reduction(
transform = exact {
universe_size = "num_sets",
num_sets = "universe_size",
}
)]
impl ReduceTo<MinimumSetCovering<i64>> for MinimumHittingSet {
type Result = ReductionHSToSC;

fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
let num_elements = self.universe_size();
// A selection has at most `num_elements` unit weights; its objective must fit `i64`.
<Self as ReduceTo<MinimumSetCovering<i64>>>::exact_i64(
num_elements,
"counting unit-weight target sets",
)?;

// D_u = { j : u in C_j }; scanning the sets in order keeps each D_u sorted.
let mut dual: Vec<Vec<usize>> = vec![Vec::new(); num_elements];
for (j, set) in self.sets().iter().enumerate() {
for &element in set {
dual[element].push(j);
}
}

let target = MinimumSetCovering::<i64>::try_with_weights(
self.num_sets(),
dual,
vec![1; num_elements],
)
.map_err(<Self as ReduceTo<MinimumSetCovering<i64>>>::target_construction)?;

Ok(ReductionHSToSC { target })
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "minimumhittingset_to_minimumsetcovering",
build: || {
let source =
MinimumHittingSet::new(5, vec![vec![0, 1], vec![1, 2, 3], vec![0, 3], vec![2, 4]]);
crate::example_db::specs::rule_example_with_witness::<_, MinimumSetCovering<i64>>(
source,
SolutionPair {
source_config: serde_json::json!(vec![true, false, true, false, false]),
target_config: serde_json::json!(vec![true, false, true, false, false]),
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/minimumhittingset_minimumsetcovering.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ pub(crate) mod minimumfeedbackarcset_ilp;
pub(crate) mod minimumfeedbackvertexset_ilp;
pub(crate) mod minimumgraphbandwidth_ilp;
pub(crate) mod minimumhittingset_ilp;
pub(crate) mod minimumhittingset_minimumsetcovering;
pub(crate) mod minimuminternalmacrodatacompression_ilp;
pub(crate) mod minimummatrixcover_ilp;
pub(crate) mod minimummaximalmatching_ilp;
Expand Down Expand Up @@ -483,6 +484,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(minimummaximalmatching_minimummatrixdomination::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumfeedbackarcset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
specs.extend(minimumhittingset_minimumsetcovering::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumhittingset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumsetcovering::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumweightandorgraph::canonical_rule_example_specs());
Expand Down
Loading
Loading