Skip to content

Commit 77577dd

Browse files
GiggleLiuclaude
andauthored
Fix #209: [Model] MinimumHittingSet (#720)
* Add plan for #209: [Model] MinimumHittingSet * Add MinimumHittingSet model core * Register MinimumHittingSet in the model catalog * Tighten MinimumHittingSet test coverage * Add MinimumHittingSet CLI creation support * Document MinimumHittingSet in the paper * chore: remove plan file after implementation * Fix merge conflicts: alphabetical ordering in set/mod.rs and rustfmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7952b3d commit 77577dd

9 files changed

Lines changed: 499 additions & 5 deletions

File tree

docs/paper/reductions.typ

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"TravelingSalesman": [Traveling Salesman],
8484
"MaximumClique": [Maximum Clique],
8585
"MaximumSetPacking": [Maximum Set Packing],
86+
"MinimumHittingSet": [Minimum Hitting Set],
8687
"MinimumSetCovering": [Minimum Set Covering],
8788
"ComparativeContainment": [Comparative Containment],
8889
"SetBasis": [Set Basis],
@@ -1823,6 +1824,56 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
18231824
]
18241825
}
18251826

1827+
#{
1828+
let x = load-model-example("MinimumHittingSet")
1829+
let sets = x.instance.sets
1830+
let m = sets.len()
1831+
let U-size = x.instance.universe_size
1832+
let sol = (config: x.optimal_config, metric: x.optimal_value)
1833+
let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
1834+
let hit-size = sol.metric.Valid
1835+
let fmt-set(s) = if s.len() == 0 {
1836+
$emptyset$
1837+
} else {
1838+
"${" + s.map(e => str(e + 1)).join(", ") + "}$"
1839+
}
1840+
let elems = (
1841+
(-2.0, 0.7),
1842+
(-0.9, 1.4),
1843+
(-1.2, -0.4),
1844+
(0.2, 0.1),
1845+
(1.2, 1.0),
1846+
(1.5, -0.9),
1847+
)
1848+
[
1849+
#problem-def("MinimumHittingSet")[
1850+
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}$.
1851+
][
1852+
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.
1853+
1854+
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.].
1855+
1856+
*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$.
1857+
1858+
#figure(
1859+
canvas(length: 1cm, {
1860+
sregion((elems.at(0), elems.at(1), elems.at(2)), pad: 0.45, label: [$S_1$], ..sregion-dimmed)
1861+
sregion((elems.at(0), elems.at(3), elems.at(4)), pad: 0.48, label: [$S_2$], ..sregion-dimmed)
1862+
sregion((elems.at(1), elems.at(3), elems.at(5)), pad: 0.48, label: [$S_3$], ..sregion-dimmed)
1863+
sregion((elems.at(2), elems.at(4), elems.at(5)), pad: 0.48, label: [$S_4$], ..sregion-dimmed)
1864+
sregion((elems.at(0), elems.at(1), elems.at(5)), pad: 0.48, label: [$S_5$], ..sregion-dimmed)
1865+
sregion((elems.at(2), elems.at(3)), pad: 0.34, label: [$S_6$], ..sregion-dimmed)
1866+
sregion((elems.at(1), elems.at(4)), pad: 0.34, label: [$S_7$], ..sregion-dimmed)
1867+
for (k, pos) in elems.enumerate() {
1868+
selem(pos, label: [#(k + 1)], fill: if selected.contains(k) { graph-colors.at(0) } else { black })
1869+
}
1870+
}),
1871+
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)$.]
1872+
) <fig:min-hitting-set>
1873+
]
1874+
]
1875+
}
1876+
18261877
#{
18271878
let x = load-model-example("ConsecutiveSets")
18281879
let m = x.instance.alphabet_size

problemreductions-cli/src/cli.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ Flags by problem type:
242242
SumOfSquaresPartition --sizes, --num-groups, --bound
243243
PaintShop --sequence
244244
MaximumSetPacking --sets [--weights]
245+
MinimumHittingSet --universe, --sets
245246
MinimumSetCovering --universe, --sets [--weights]
246247
ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights]
247248
X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each)
@@ -438,7 +439,7 @@ pub struct CreateArgs {
438439
/// Car paint sequence for PaintShop (comma-separated, each label appears exactly twice, e.g., "a,b,a,c,c,b")
439440
#[arg(long)]
440441
pub sequence: Option<String>,
441-
/// Sets for SetPacking/SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2")
442+
/// Sets for set-system problems such as SetPacking, MinimumHittingSet, and SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2")
442443
#[arg(long)]
443444
pub sets: Option<String>,
444445
/// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2")
@@ -456,7 +457,7 @@ pub struct CreateArgs {
456457
/// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3")
457458
#[arg(long)]
458459
pub partition: Option<String>,
459-
/// Universe size for set-system problems such as MinimumSetCovering and ComparativeContainment
460+
/// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment
460461
#[arg(long)]
461462
pub universe: Option<usize>,
462463
/// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs)

problemreductions-cli/src/commands/create.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
19481948
)
19491949
}
19501950

1951+
// MinimumHittingSet
1952+
"MinimumHittingSet" => {
1953+
let universe = args.universe.ok_or_else(|| {
1954+
anyhow::anyhow!(
1955+
"MinimumHittingSet requires --universe and --sets\n\n\
1956+
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\""
1957+
)
1958+
})?;
1959+
let sets = parse_sets(args)?;
1960+
for (i, set) in sets.iter().enumerate() {
1961+
for &element in set {
1962+
if element >= universe {
1963+
bail!(
1964+
"Set {} contains element {} which is outside universe of size {}",
1965+
i,
1966+
element,
1967+
universe
1968+
);
1969+
}
1970+
}
1971+
}
1972+
(
1973+
ser(MinimumHittingSet::new(universe, sets))?,
1974+
resolved_variant.clone(),
1975+
)
1976+
}
1977+
19511978
// MinimumSetCovering
19521979
"MinimumSetCovering" => {
19531980
let universe = args.universe.ok_or_else(|| {

problemreductions-cli/tests/cli_tests.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,86 @@ fn test_create_comparative_containment_no_flags_shows_help() {
14251425
assert!(!stderr.contains("--universe-size"), "stderr: {stderr}");
14261426
}
14271427

1428+
#[test]
1429+
fn test_create_minimum_hitting_set() {
1430+
let output_file = std::env::temp_dir().join("pred_test_create_minimum_hitting_set.json");
1431+
let output = pred()
1432+
.args([
1433+
"-o",
1434+
output_file.to_str().unwrap(),
1435+
"create",
1436+
"MinimumHittingSet",
1437+
"--universe",
1438+
"6",
1439+
"--sets",
1440+
"0,1,2;0,3,4;1,3,5;2,4,5;0,1,5;2,3;1,4",
1441+
])
1442+
.output()
1443+
.unwrap();
1444+
assert!(
1445+
output.status.success(),
1446+
"stderr: {}",
1447+
String::from_utf8_lossy(&output.stderr)
1448+
);
1449+
assert!(output_file.exists());
1450+
1451+
let content = std::fs::read_to_string(&output_file).unwrap();
1452+
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1453+
assert_eq!(json["type"], "MinimumHittingSet");
1454+
assert_eq!(json["data"]["universe_size"], 6);
1455+
assert_eq!(
1456+
json["data"]["sets"],
1457+
serde_json::json!([
1458+
[0, 1, 2],
1459+
[0, 3, 4],
1460+
[1, 3, 5],
1461+
[2, 4, 5],
1462+
[0, 1, 5],
1463+
[2, 3],
1464+
[1, 4]
1465+
])
1466+
);
1467+
1468+
std::fs::remove_file(&output_file).ok();
1469+
}
1470+
1471+
#[test]
1472+
fn test_create_minimum_hitting_set_rejects_out_of_range_elements_without_panicking() {
1473+
let output = pred()
1474+
.args([
1475+
"create",
1476+
"MinimumHittingSet",
1477+
"--universe",
1478+
"4",
1479+
"--sets",
1480+
"0,1,4;1,2",
1481+
])
1482+
.output()
1483+
.unwrap();
1484+
assert!(!output.status.success());
1485+
let stderr = String::from_utf8_lossy(&output.stderr);
1486+
assert!(
1487+
stderr.contains("outside universe of size 4"),
1488+
"stderr: {stderr}"
1489+
);
1490+
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
1491+
}
1492+
1493+
#[test]
1494+
fn test_create_help_lists_minimum_hitting_set_flags() {
1495+
let output = pred().args(["create", "--help"]).output().unwrap();
1496+
assert!(
1497+
output.status.success(),
1498+
"stderr: {}",
1499+
String::from_utf8_lossy(&output.stderr)
1500+
);
1501+
let stdout = String::from_utf8(output.stdout).unwrap();
1502+
assert!(
1503+
stdout.contains("MinimumHittingSet") && stdout.contains("--universe, --sets"),
1504+
"stdout: {stdout}"
1505+
);
1506+
}
1507+
14281508
#[test]
14291509
fn test_create_set_basis_requires_k() {
14301510
let output = pred()

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub mod prelude {
7777
};
7878
pub use crate::models::set::{
7979
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
80-
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
80+
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
8181
};
8282

8383
// Core traits

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ pub use misc::{
4444
};
4545
pub use set::{
4646
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
47-
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
47+
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
4848
TwoDimensionalConsecutiveSets,
4949
};

0 commit comments

Comments
 (0)