Skip to content
51 changes: 51 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"TravelingSalesman": [Traveling Salesman],
"MaximumClique": [Maximum Clique],
"MaximumSetPacking": [Maximum Set Packing],
"MinimumHittingSet": [Minimum Hitting Set],
"MinimumSetCovering": [Minimum Set Covering],
"ComparativeContainment": [Comparative Containment],
"SetBasis": [Set Basis],
Expand Down Expand Up @@ -1794,6 +1795,56 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("MinimumHittingSet")
let sets = x.instance.sets
let m = sets.len()
let U-size = x.instance.universe_size
let sol = (config: x.optimal_config, metric: x.optimal_value)
let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
let hit-size = sol.metric.Valid
let fmt-set(s) = if s.len() == 0 {
$emptyset$
} else {
"${" + s.map(e => str(e + 1)).join(", ") + "}$"
}
let elems = (
(-2.0, 0.7),
(-0.9, 1.4),
(-1.2, -0.4),
(0.2, 0.1),
(1.2, 1.0),
(1.5, -0.9),
)
[
#problem-def("MinimumHittingSet")[
Given a finite universe $U$ and a collection $cal(S) = {S_1, dots, S_m}$ of subsets of $U$, find a subset $H subset.eq U$ minimizing $|H|$ such that $H inter S_i != emptyset$ for every $i in {1, dots, m}$.
][
Minimum Hitting Set is one of Karp's 21 NP-complete problems @karp1972. It is the incidence-dual of Set Covering: transposing the set-element incidence matrix swaps the choice of sets with the choice of universe elements. Vertex Cover is the special case in which every set has size $2$, so every edge is "hit" by selecting one of its endpoints.

A direct exact algorithm enumerates all $2^n$ subsets $H subset.eq U$ for $n = |U|$ and checks whether each subset intersects every member of $cal(S)$. This yields an $O^*(2^n)$ exact algorithm#footnote[No exact worst-case algorithm improving on brute-force enumeration over the universe elements is recorded in the standard references used for this catalog entry.].

*Example.* Let $U = {1, 2, dots, #U-size}$ and $cal(S) = {#range(m).map(i => $S_#(i + 1)$).join(", ")}$ with #range(m).map(i => $S_#(i + 1) = #fmt-set(sets.at(i))$).join(", "). A minimum hitting set is $H = #fmt-set(selected)$ with $|H| = #hit-size$: every set in $cal(S)$ contains at least one of the selected elements. No $2$-element subset of $U$ hits all #m sets, so the optimum is exactly $#hit-size$.

#figure(
canvas(length: 1cm, {
sregion((elems.at(0), elems.at(1), elems.at(2)), pad: 0.45, label: [$S_1$], ..sregion-dimmed)
sregion((elems.at(0), elems.at(3), elems.at(4)), pad: 0.48, label: [$S_2$], ..sregion-dimmed)
sregion((elems.at(1), elems.at(3), elems.at(5)), pad: 0.48, label: [$S_3$], ..sregion-dimmed)
sregion((elems.at(2), elems.at(4), elems.at(5)), pad: 0.48, label: [$S_4$], ..sregion-dimmed)
sregion((elems.at(0), elems.at(1), elems.at(5)), pad: 0.48, label: [$S_5$], ..sregion-dimmed)
sregion((elems.at(2), elems.at(3)), pad: 0.34, label: [$S_6$], ..sregion-dimmed)
sregion((elems.at(1), elems.at(4)), pad: 0.34, label: [$S_7$], ..sregion-dimmed)
for (k, pos) in elems.enumerate() {
selem(pos, label: [#(k + 1)], fill: if selected.contains(k) { graph-colors.at(0) } else { black })
}
}),
caption: [Minimum hitting set: the blue elements $#fmt-set(selected)$ intersect every set region $S_1, dots, S_#m$, so they hit the entire collection $cal(S)$.]
) <fig:min-hitting-set>
]
]
}

#{
let x = load-model-example("ConsecutiveSets")
let m = x.instance.alphabet_size
Expand Down
5 changes: 3 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ Flags by problem type:
SumOfSquaresPartition --sizes, --num-groups, --bound
PaintShop --sequence
MaximumSetPacking --sets [--weights]
MinimumHittingSet --universe, --sets
MinimumSetCovering --universe, --sets [--weights]
ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights]
X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each)
Expand Down Expand Up @@ -434,7 +435,7 @@ pub struct CreateArgs {
/// Car paint sequence for PaintShop (comma-separated, each label appears exactly twice, e.g., "a,b,a,c,c,b")
#[arg(long)]
pub sequence: Option<String>,
/// Sets for SetPacking/SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2")
/// Sets for set-system problems such as SetPacking, MinimumHittingSet, and SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2")
#[arg(long)]
pub sets: Option<String>,
/// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2")
Expand All @@ -452,7 +453,7 @@ pub struct CreateArgs {
/// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3")
#[arg(long)]
pub partition: Option<String>,
/// Universe size for set-system problems such as MinimumSetCovering and ComparativeContainment
/// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment
#[arg(long)]
pub universe: Option<usize>,
/// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs)
Expand Down
27 changes: 27 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinimumHittingSet
"MinimumHittingSet" => {
let universe = args.universe.ok_or_else(|| {
anyhow::anyhow!(
"MinimumHittingSet requires --universe and --sets\n\n\
Usage: pred create MinimumHittingSet --universe 6 --sets \"0,1,2;0,3,4;1,3,5;2,4,5;0,1,5;2,3;1,4\""
)
})?;
let sets = parse_sets(args)?;
for (i, set) in sets.iter().enumerate() {
for &element in set {
if element >= universe {
bail!(
"Set {} contains element {} which is outside universe of size {}",
i,
element,
universe
);
}
}
}
(
ser(MinimumHittingSet::new(universe, sets))?,
resolved_variant.clone(),
)
}

// MinimumSetCovering
"MinimumSetCovering" => {
let universe = args.universe.ok_or_else(|| {
Expand Down
80 changes: 80 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,86 @@ fn test_create_comparative_containment_no_flags_shows_help() {
assert!(!stderr.contains("--universe-size"), "stderr: {stderr}");
}

#[test]
fn test_create_minimum_hitting_set() {
let output_file = std::env::temp_dir().join("pred_test_create_minimum_hitting_set.json");
let output = pred()
.args([
"-o",
output_file.to_str().unwrap(),
"create",
"MinimumHittingSet",
"--universe",
"6",
"--sets",
"0,1,2;0,3,4;1,3,5;2,4,5;0,1,5;2,3;1,4",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output_file.exists());

let content = std::fs::read_to_string(&output_file).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json["type"], "MinimumHittingSet");
assert_eq!(json["data"]["universe_size"], 6);
assert_eq!(
json["data"]["sets"],
serde_json::json!([
[0, 1, 2],
[0, 3, 4],
[1, 3, 5],
[2, 4, 5],
[0, 1, 5],
[2, 3],
[1, 4]
])
);

std::fs::remove_file(&output_file).ok();
}

#[test]
fn test_create_minimum_hitting_set_rejects_out_of_range_elements_without_panicking() {
let output = pred()
.args([
"create",
"MinimumHittingSet",
"--universe",
"4",
"--sets",
"0,1,4;1,2",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("outside universe of size 4"),
"stderr: {stderr}"
);
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
}

#[test]
fn test_create_help_lists_minimum_hitting_set_flags() {
let output = pred().args(["create", "--help"]).output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("MinimumHittingSet") && stdout.contains("--universe, --sets"),
"stdout: {stdout}"
);
}

#[test]
fn test_create_set_basis_requires_k() {
let output = pred()
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub mod prelude {
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
};

// Core traits
Expand Down
2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ pub use misc::{
};
pub use set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
TwoDimensionalConsecutiveSets,
};
Loading
Loading