From 3f1e4ce505622d5894812931fdf1013cde9ae0ae Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 13:52:17 +0800 Subject: [PATCH 1/8] Add plan for #209: [Model] MinimumHittingSet --- docs/plans/2026-03-21-minimum-hitting-set.md | 273 +++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 docs/plans/2026-03-21-minimum-hitting-set.md diff --git a/docs/plans/2026-03-21-minimum-hitting-set.md b/docs/plans/2026-03-21-minimum-hitting-set.md new file mode 100644 index 000000000..ebc4d9da7 --- /dev/null +++ b/docs/plans/2026-03-21-minimum-hitting-set.md @@ -0,0 +1,273 @@ +# MinimumHittingSet Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `MinimumHittingSet` as a new set-based optimization model for issue `#209`, with brute-force support, canonical example data, CLI creation support, and paper documentation. + +**Architecture:** Implement `MinimumHittingSet` as a new `src/models/set/` optimization model whose binary variables correspond to universe elements, not sets. Reuse the `MinimumSetCovering` registration and example-db patterns, but define `dims()` over `universe_size`, treat a configuration as valid iff every input set contains a selected element, and minimize the number of selected elements. Keep this PR model-only: no reduction rules or ILP reduction in this branch. + +**Tech Stack:** Rust workspace, `inventory` schema registry, `declare_variants!`, `BruteForce`, Typst paper, CLI `pred create` + +--- + +## Issue Checklist + +| Item | Value | +|---|---| +| Problem name | `MinimumHittingSet` | +| Category | `set` | +| Problem type | Optimization (`Direction::Minimize`) | +| Struct fields | `universe_size: usize`, `sets: Vec>` | +| Configuration space | `vec![2; universe_size]` | +| Feasibility rule | Every set in `sets` contains at least one selected universe element | +| Objective | Minimize the number of selected elements | +| Complexity string | `"2^universe_size"` | +| Solver for this PR | `BruteForce` only | +| Canonical example outcome | Universe `{0,1,2,3,4,5}`, sets `[{0,1,2},{0,3,4},{1,3,5},{2,4,5},{0,1,5},{2,3},{1,4}]`, optimal hitting set `{1,3,4}` with config `[0,1,0,1,1,0]` and value `3` | +| Associated open rules | `#200`, `#460`, `#462`, `#467` | + +## Design Notes + +- Use `SolutionSize` instead of a weight parameter. The issue explicitly models cardinality minimization, so a generic weight dimension would add unsupported surface area. +- Add `universe_size()` and `num_sets()` getters. `universe_size()` is required by the complexity string and `num_sets()` is useful for diagnostics/tests. +- Normalize each input set in the constructor by sorting/deduplicating it and assert that every element index is `< universe_size`. This prevents panics during evaluation on malformed instances. +- Treat empty input sets as making the instance unsatisfiable: every configuration should evaluate to `Invalid` if any set is empty. +- Do not add a short alias like `MHS`; the repo only adds short aliases that are standard in the literature. + +## Batch 1: Model, Registry, Tests, and CLI + +### Task 1: Add the failing model tests first + +**Files:** +- Create: `src/unit_tests/models/set/minimum_hitting_set.rs` +- Modify: `src/unit_tests/problem_size.rs` +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `src/lib.rs` (only if a missing re-export breaks test compilation) + +**Step 1: Write the failing tests** + +Add tests for: +- creation/accessors/dimensions +- valid vs invalid evaluation +- constructor normalization / out-of-range rejection +- brute-force optimum on the issue example +- serialization round-trip +- paper example consistency (`[0,1,0,1,1,0]` gives `Valid(3)`) +- `problem_size()` exposes `universe_size` and `num_sets` +- generic trait-consistency coverage includes `MinimumHittingSet` + +**Step 2: Run the targeted tests to verify RED** + +Run: + +```bash +cargo test minimum_hitting_set --lib +``` + +Expected: compile failure because `MinimumHittingSet` does not exist yet. + +**Step 3: Write the minimal implementation to satisfy the tests** + +Create `src/models/set/minimum_hitting_set.rs` with: +- `ProblemSchemaEntry` registration +- `MinimumHittingSet` struct with `new()`, `universe_size()`, `num_sets()`, `sets()`, `get_set()` +- helpers such as `selected_elements()` / `is_valid_solution()` as needed +- `Problem` impl with `Metric = SolutionSize`, `dims() = vec![2; universe_size]`, `evaluate()` +- `OptimizationProblem` impl with `Direction::Minimize` +- `declare_variants! { default opt MinimumHittingSet => "2^universe_size", }` +- `#[cfg(test)]` link to the new unit test file + +Mirror the style of `src/models/set/minimum_set_covering.rs`, but keep the semantics element-based rather than set-based. + +**Step 4: Run the targeted tests to verify GREEN** + +Run: + +```bash +cargo test minimum_hitting_set --lib +``` + +Expected: the new model tests pass. + +**Step 5: Commit** + +```bash +git add src/models/set/minimum_hitting_set.rs src/unit_tests/models/set/minimum_hitting_set.rs src/unit_tests/problem_size.rs src/unit_tests/trait_consistency.rs +git commit -m "Add MinimumHittingSet model core" +``` + +### Task 2: Register the model and canonical example data + +**Files:** +- Modify: `src/models/set/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `src/models/set/minimum_hitting_set.rs` + +**Step 1: Add the failing example/registration checks** + +Extend the new model tests so they exercise: +- crate-level/prelude imports if needed +- `canonical_model_example_specs()` contents for `MinimumHittingSet` +- brute-force optimal config/value for the canonical example + +**Step 2: Run the focused tests to verify RED** + +Run: + +```bash +cargo test minimum_hitting_set --features example-db --lib +``` + +Expected: failure until the model is exported and its canonical example is registered. + +**Step 3: Wire the model into the registry surface** + +- Add `pub(crate) mod minimum_hitting_set;` and `pub use minimum_hitting_set::MinimumHittingSet;` in `src/models/set/mod.rs` +- Extend the set example chain with `minimum_hitting_set::canonical_model_example_specs()` +- Re-export `MinimumHittingSet` from `src/models/mod.rs` and the crate prelude in `src/lib.rs` +- Add `#[cfg(feature = "example-db")] canonical_model_example_specs()` in the model file using the issue example and `optimal_value = {"Valid": 3}` + +**Step 4: Run the focused tests to verify GREEN** + +Run: + +```bash +cargo test minimum_hitting_set --features example-db --lib +``` + +Expected: the example-db aware tests pass. + +**Step 5: Commit** + +```bash +git add src/models/set/mod.rs src/models/mod.rs src/lib.rs src/models/set/minimum_hitting_set.rs +git commit -m "Register MinimumHittingSet in the model catalog" +``` + +### Task 3: Add CLI creation support and CLI tests + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` only if a lowercase full-name lookup test proves it is needed + +**Step 1: Write the failing CLI tests** + +Add CLI coverage for: +- `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"` +- optional `--example` flow if existing set-model tests cover that pattern +- help table text includes `MinimumHittingSet --universe, --sets` + +**Step 2: Run the targeted CLI tests to verify RED** + +Run: + +```bash +cargo test -p problemreductions-cli minimum_hitting_set +``` + +Expected: failure because CLI creation/help do not know the new model yet. + +**Step 3: Implement the minimal CLI support** + +- Add a `create()` match arm analogous to `MinimumSetCovering`, but call `MinimumHittingSet::new(universe, sets)` +- Update the create-help table in `problemreductions-cli/src/cli.rs` +- Only touch alias resolution if the tests show the registry does not already resolve the canonical name adequately + +**Step 4: Run the targeted CLI tests to verify GREEN** + +Run: + +```bash +cargo test -p problemreductions-cli minimum_hitting_set +``` + +Expected: the new CLI tests pass. + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs problemreductions-cli/tests/cli_tests.rs problemreductions-cli/src/problem_name.rs +git commit -m "Add MinimumHittingSet CLI creation support" +``` + +## Batch 2: Paper Entry + +### Task 4: Document the model in the paper after the implementation exists + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` only if the chosen citations are missing +- Modify: `src/unit_tests/models/set/minimum_hitting_set.rs` if the paper example wording requires tightening the paper-example assertions + +**Step 1: Write or tighten the failing paper-example test** + +Ensure the unit test enforces the issue example exactly: +- instance matches the paper example +- config `[0,1,0,1,1,0]` is valid +- brute-force confirms optimum value `3` + +**Step 2: Run the paper-example test to verify RED if needed** + +Run: + +```bash +cargo test test_minimum_hitting_set_paper_example --lib +``` + +Expected: fail only if the paper-facing example and code diverge. + +**Step 3: Add the paper entry** + +In `docs/paper/reductions.typ`: +- add `"MinimumHittingSet": [Minimum Hitting Set]` to `display-name` +- add a `problem-def("MinimumHittingSet")` entry near the other set problems +- cite `@karp1972` for historical context +- explain the duality with Set Covering and the Vertex Cover special case +- use the issue example with a set-system diagram and the optimal hitting set `{1,3,4}` + +Prefer the `MinimumSetCovering` entry as the layout template, but highlight selected universe elements instead of selected sets. + +**Step 4: Verify the paper build** + +Run: + +```bash +cargo test test_minimum_hitting_set_paper_example --lib +make paper +``` + +Expected: the paper example test passes and the Typst build succeeds. + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ docs/paper/references.bib src/unit_tests/models/set/minimum_hitting_set.rs +git commit -m "Document MinimumHittingSet in the paper" +``` + +## Final Verification + +After both batches are complete, run: + +```bash +make test +make clippy +git status --short +``` + +Expected: +- tests pass +- clippy passes +- only intended tracked files are modified + +## issue-to-pr Cleanup + +After implementation is committed and verified: + +1. Delete this file with `git rm docs/plans/2026-03-21-minimum-hitting-set.md` +2. Commit the removal: `git commit -m "chore: remove plan file after implementation"` +3. Post the PR implementation summary comment +4. Push the branch From 28f38cbea2b95f8908d4d80d4dc6e3e4892e4f05 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 14:10:49 +0800 Subject: [PATCH 2/8] Add MinimumHittingSet model core --- src/models/mod.rs | 2 +- src/models/set/minimum_hitting_set.rs | 157 ++++++++++++++++++ src/models/set/mod.rs | 2 + .../models/set/minimum_hitting_set.rs | 113 +++++++++++++ src/unit_tests/problem_size.rs | 8 + src/unit_tests/trait_consistency.rs | 8 + 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/models/set/minimum_hitting_set.rs create mode 100644 src/unit_tests/models/set/minimum_hitting_set.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 61f21e9f8..992c6c9f2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -42,6 +42,6 @@ pub use misc::{ }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, - MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis, + MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis, TwoDimensionalConsecutiveSets, }; diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs new file mode 100644 index 000000000..85cd5eec2 --- /dev/null +++ b/src/models/set/minimum_hitting_set.rs @@ -0,0 +1,157 @@ +//! Minimum Hitting Set problem implementation. +//! +//! The Minimum Hitting Set problem asks for a minimum-size subset of universe +//! elements that intersects every set in a collection. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumHittingSet", + display_name: "Minimum Hitting Set", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Find a minimum-size subset of universe elements that hits every set", + fields: &[ + FieldInfo { name: "universe_size", type_name: "usize", description: "Size of the universe U" }, + FieldInfo { name: "sets", type_name: "Vec>", description: "Collection of subsets of U that must each be hit" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "MinimumHittingSet", + fields: &["num_sets", "universe_size"], + } +} + +/// The Minimum Hitting Set problem. +/// +/// Given a universe `U` and a collection of subsets of `U`, find a minimum-size +/// subset `H ⊆ U` such that `H` intersects every set in the collection. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumHittingSet { + universe_size: usize, + sets: Vec>, +} + +impl MinimumHittingSet { + /// Create a new Minimum Hitting Set instance. + /// + /// # Panics + /// + /// Panics if any set contains an element outside `0..universe_size`. + pub fn new(universe_size: usize, sets: Vec>) -> Self { + let mut sets = sets; + for (set_index, set) in sets.iter_mut().enumerate() { + set.sort_unstable(); + set.dedup(); + for &element in set.iter() { + assert!( + element < universe_size, + "Set {set_index} contains element {element} which is outside universe of size {universe_size}" + ); + } + } + + Self { universe_size, sets } + } + + /// Get the universe size. + pub fn universe_size(&self) -> usize { + self.universe_size + } + + /// Get the number of sets. + pub fn num_sets(&self) -> usize { + self.sets.len() + } + + /// Get the sets. + pub fn sets(&self) -> &[Vec] { + &self.sets + } + + /// Get a specific set. + pub fn get_set(&self, index: usize) -> Option<&Vec> { + self.sets.get(index) + } + + /// Decode the selected universe elements from a binary configuration. + pub fn selected_elements(&self, config: &[usize]) -> Option> { + if config.len() != self.universe_size { + return None; + } + + let mut selected = Vec::new(); + for (element, &value) in config.iter().enumerate() { + match value { + 0 => {} + 1 => selected.push(element), + _ => return None, + } + } + Some(selected) + } + + /// Check whether a configuration hits every set in the collection. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + let Some(selected) = self.selected_elements(config) else { + return false; + }; + + self.sets + .iter() + .all(|set| set.iter().any(|element| selected.binary_search(element).is_ok())) + } +} + +impl Problem for MinimumHittingSet { + const NAME: &'static str = "MinimumHittingSet"; + type Metric = SolutionSize; + + fn dims(&self) -> Vec { + vec![2; self.universe_size] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + let Some(selected) = self.selected_elements(config) else { + return SolutionSize::Invalid; + }; + + if self + .sets + .iter() + .all(|set| set.iter().any(|element| selected.binary_search(element).is_ok())) + { + SolutionSize::Valid(selected.len()) + } else { + SolutionSize::Invalid + } + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl OptimizationProblem for MinimumHittingSet { + type Value = usize; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + default opt MinimumHittingSet => "2^universe_size", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/set/minimum_hitting_set.rs"] +mod tests; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index b0a101a42..1ad6a8839 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -12,6 +12,7 @@ pub(crate) mod comparative_containment; pub(crate) mod consecutive_sets; pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; +pub(crate) mod minimum_hitting_set; pub(crate) mod minimum_cardinality_key; pub(crate) mod minimum_set_covering; pub(crate) mod prime_attribute_name; @@ -22,6 +23,7 @@ pub use comparative_containment::ComparativeContainment; pub use consecutive_sets::ConsecutiveSets; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; +pub use minimum_hitting_set::MinimumHittingSet; pub use minimum_cardinality_key::MinimumCardinalityKey; pub use minimum_set_covering::MinimumSetCovering; pub use prime_attribute_name::PrimeAttributeName; diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs new file mode 100644 index 000000000..fcaf77ae9 --- /dev/null +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -0,0 +1,113 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; +use crate::types::SolutionSize; +use std::collections::HashSet; + +fn issue_example_problem() -> MinimumHittingSet { + MinimumHittingSet::new( + 6, + vec![ + vec![0, 1, 2], + vec![0, 3, 4], + vec![1, 3, 5], + vec![2, 4, 5], + vec![0, 1, 5], + vec![2, 3], + vec![1, 4], + ], + ) +} + +fn issue_example_config() -> Vec { + vec![0, 1, 0, 1, 1, 0] +} + +#[test] +fn test_minimum_hitting_set_creation_accessors_and_dimensions() { + let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3]]); + + assert_eq!(problem.universe_size(), 4); + assert_eq!(problem.num_sets(), 2); + assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.dims(), vec![2; 4]); + assert_eq!(problem.sets(), &[vec![1, 2], vec![3]]); + assert_eq!(problem.get_set(0), Some(&vec![1, 2])); + assert_eq!(problem.get_set(1), Some(&vec![3])); + assert_eq!(problem.get_set(2), None); +} + +#[test] +fn test_minimum_hitting_set_evaluate_valid_and_invalid() { + let problem = MinimumHittingSet::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + + assert_eq!(problem.selected_elements(&[0, 1, 0, 1]), Some(vec![1, 3])); + assert_eq!(problem.selected_elements(&[0, 2, 0, 1]), None); + assert_eq!(problem.evaluate(&[0, 1, 0, 1]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 2, 0, 1]), SolutionSize::Invalid); + assert!(problem.is_valid_solution(&[0, 1, 0, 1])); + assert!(!problem.is_valid_solution(&[1, 0, 0, 0])); + assert!(!problem.is_valid_solution(&[0, 2, 0, 1])); +} + +#[test] +fn test_minimum_hitting_set_empty_set_is_always_invalid() { + let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![]]); + + assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_hitting_set_constructor_normalizes_sets() { + let problem = MinimumHittingSet::new(5, vec![vec![3, 1, 3, 2], vec![4, 0, 0], vec![]]); + + assert_eq!(problem.sets(), &[vec![1, 2, 3], vec![0, 4], vec![]]); +} + +#[test] +#[should_panic(expected = "outside universe")] +fn test_minimum_hitting_set_rejects_out_of_range_elements() { + MinimumHittingSet::new(3, vec![vec![0, 3]]); +} + +#[test] +fn test_minimum_hitting_set_bruteforce_optimum_issue_example() { + let problem = issue_example_problem(); + let solver = BruteForce::new(); + + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + + let best_solutions = solver.find_all_best(&problem); + let best_solution_set: HashSet> = best_solutions.iter().cloned().collect(); + assert!(best_solution_set.contains(&issue_example_config())); + assert!( + best_solutions + .iter() + .all(|config| problem.evaluate(config) == SolutionSize::Valid(3)) + ); +} + +#[test] +fn test_minimum_hitting_set_serialization_round_trip() { + let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3, 0]]); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: MinimumHittingSet = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.universe_size(), problem.universe_size()); + assert_eq!(deserialized.num_sets(), problem.num_sets()); + assert_eq!(deserialized.sets(), problem.sets()); + assert_eq!( + deserialized.evaluate(&[1, 1, 0, 0]), + problem.evaluate(&[1, 1, 0, 0]) + ); +} + +#[test] +fn test_minimum_hitting_set_paper_example_consistency() { + let problem = issue_example_problem(); + + assert_eq!(problem.evaluate(&issue_example_config()), SolutionSize::Valid(3)); +} diff --git a/src/unit_tests/problem_size.rs b/src/unit_tests/problem_size.rs index 1e40e683f..46eb4b5a9 100644 --- a/src/unit_tests/problem_size.rs +++ b/src/unit_tests/problem_size.rs @@ -218,3 +218,11 @@ fn test_problem_size_set_covering() { assert_eq!(size.get("num_sets"), Some(3)); assert_eq!(size.get("universe_size"), Some(4)); } + +#[test] +fn test_problem_size_minimum_hitting_set() { + let hs = MinimumHittingSet::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let size = problem_size(&hs); + assert_eq!(size.get("num_sets"), Some(3)); + assert_eq!(size.get("universe_size"), Some(4)); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index f45862e05..1de146d8c 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -70,6 +70,10 @@ fn test_all_problems_implement_trait_correctly() { &MinimumSetCovering::::new(3, vec![vec![0, 1]]), "MinimumSetCovering", ); + check_problem_trait( + &MinimumHittingSet::new(3, vec![vec![0, 1]]), + "MinimumHittingSet", + ); check_problem_trait( &MaximumSetPacking::::new(vec![vec![0, 1]]), "MaximumSetPacking", @@ -256,6 +260,10 @@ fn test_direction() { MinimumSetCovering::::new(2, vec![vec![0, 1]]).direction(), Direction::Minimize ); + assert_eq!( + MinimumHittingSet::new(2, vec![vec![0, 1]]).direction(), + Direction::Minimize + ); assert_eq!( PaintShop::new(vec!["a", "a"]).direction(), Direction::Minimize From 06dc92e1211f9f32e905bd1bed97878ed3516548 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 14:12:14 +0800 Subject: [PATCH 3/8] Register MinimumHittingSet in the model catalog --- src/lib.rs | 3 ++- src/models/set/minimum_hitting_set.rs | 21 +++++++++++++++++++ src/models/set/mod.rs | 2 ++ .../models/set/minimum_hitting_set.rs | 20 ++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1c051bb98..af82ff123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,8 @@ pub mod prelude { }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, - MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis, + MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, + SetBasis, }; // Core traits diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 85cd5eec2..896c8d04e 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -152,6 +152,27 @@ crate::declare_variants! { default opt MinimumHittingSet => "2^universe_size", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_hitting_set", + instance: Box::new(MinimumHittingSet::new( + 6, + vec![ + vec![0, 1, 2], + vec![0, 3, 4], + vec![1, 3, 5], + vec![2, 4, 5], + vec![0, 1, 5], + vec![2, 3], + vec![1, 4], + ], + )), + optimal_config: vec![0, 1, 0, 1, 1, 0], + optimal_value: serde_json::json!({"Valid": 3}), + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/set/minimum_hitting_set.rs"] mod tests; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index 1ad6a8839..21733a0d8 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -5,6 +5,7 @@ //! - [`ExactCoverBy3Sets`]: Exact cover by 3-element subsets (X3C) //! - [`ComparativeContainment`]: Compare containment-weight sums for two set families //! - [`MaximumSetPacking`]: Maximum weight set packing +//! - [`MinimumHittingSet`]: Minimum-size universe subset hitting every set //! - [`MinimumSetCovering`]: Minimum weight set cover //! - [`PrimeAttributeName`]: Determine if an attribute belongs to any candidate key @@ -37,6 +38,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Date: Sat, 21 Mar 2026 14:17:36 +0800 Subject: [PATCH 4/8] Tighten MinimumHittingSet test coverage --- .../models/set/minimum_hitting_set.rs | 21 +++++++++++++++++-- src/unit_tests/problem_size.rs | 8 ------- src/unit_tests/trait_consistency.rs | 8 ------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index 9327a3a76..b5d5d35bd 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -1,7 +1,8 @@ use super::*; +use crate::registry::declared_size_fields; use crate::solvers::{BruteForce, Solver}; -use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; use std::collections::HashSet; fn issue_example_problem() -> MinimumHittingSet { @@ -112,6 +113,22 @@ fn test_minimum_hitting_set_paper_example_consistency() { assert_eq!(problem.evaluate(&issue_example_config()), SolutionSize::Valid(3)); } +#[test] +fn test_minimum_hitting_set_direction() { + let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_minimum_hitting_set_declares_problem_size_fields() { + let fields: HashSet<&'static str> = + declared_size_fields("MinimumHittingSet").into_iter().collect(); + assert_eq!( + fields, + HashSet::from(["num_sets", "universe_size"]), + ); +} + #[cfg(feature = "example-db")] #[test] fn test_minimum_hitting_set_canonical_example_spec() { diff --git a/src/unit_tests/problem_size.rs b/src/unit_tests/problem_size.rs index 46eb4b5a9..1e40e683f 100644 --- a/src/unit_tests/problem_size.rs +++ b/src/unit_tests/problem_size.rs @@ -218,11 +218,3 @@ fn test_problem_size_set_covering() { assert_eq!(size.get("num_sets"), Some(3)); assert_eq!(size.get("universe_size"), Some(4)); } - -#[test] -fn test_problem_size_minimum_hitting_set() { - let hs = MinimumHittingSet::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); - let size = problem_size(&hs); - assert_eq!(size.get("num_sets"), Some(3)); - assert_eq!(size.get("universe_size"), Some(4)); -} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 1de146d8c..f45862e05 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -70,10 +70,6 @@ fn test_all_problems_implement_trait_correctly() { &MinimumSetCovering::::new(3, vec![vec![0, 1]]), "MinimumSetCovering", ); - check_problem_trait( - &MinimumHittingSet::new(3, vec![vec![0, 1]]), - "MinimumHittingSet", - ); check_problem_trait( &MaximumSetPacking::::new(vec![vec![0, 1]]), "MaximumSetPacking", @@ -260,10 +256,6 @@ fn test_direction() { MinimumSetCovering::::new(2, vec![vec![0, 1]]).direction(), Direction::Minimize ); - assert_eq!( - MinimumHittingSet::new(2, vec![vec![0, 1]]).direction(), - Direction::Minimize - ); assert_eq!( PaintShop::new(vec!["a", "a"]).direction(), Direction::Minimize From 3e9b3193031804ff08825d287f23b8c14a38fbda Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 14:17:42 +0800 Subject: [PATCH 5/8] Add MinimumHittingSet CLI creation support --- problemreductions-cli/src/cli.rs | 5 +- problemreductions-cli/src/commands/create.rs | 24 ++++++ problemreductions-cli/tests/cli_tests.rs | 80 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e2bca054e..d0e027629 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -240,6 +240,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) @@ -433,7 +434,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, - /// 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, /// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2") @@ -451,7 +452,7 @@ pub struct CreateArgs { /// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3") #[arg(long)] pub partition: Option, - /// 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, /// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 878ee9bac..0dc5605c7 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1730,6 +1730,30 @@ 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(|| { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index a6aa1b924..f7a5b8cf8 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -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() From be495ed17fac1f1b42def1cdfefda6b69d6c5d4a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 14:20:03 +0800 Subject: [PATCH 6/8] Document MinimumHittingSet in the paper --- docs/paper/reductions.typ | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a87476d43..a95678b25 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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], @@ -1793,6 +1794,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)$.] + ) + ] + ] +} + #{ let x = load-model-example("ConsecutiveSets") let m = x.instance.alphabet_size From 9515de5ed629c1e6bd97d351df49331f16ed4373 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 14:23:10 +0800 Subject: [PATCH 7/8] chore: remove plan file after implementation --- docs/plans/2026-03-21-minimum-hitting-set.md | 273 ------------------- 1 file changed, 273 deletions(-) delete mode 100644 docs/plans/2026-03-21-minimum-hitting-set.md diff --git a/docs/plans/2026-03-21-minimum-hitting-set.md b/docs/plans/2026-03-21-minimum-hitting-set.md deleted file mode 100644 index ebc4d9da7..000000000 --- a/docs/plans/2026-03-21-minimum-hitting-set.md +++ /dev/null @@ -1,273 +0,0 @@ -# MinimumHittingSet Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `MinimumHittingSet` as a new set-based optimization model for issue `#209`, with brute-force support, canonical example data, CLI creation support, and paper documentation. - -**Architecture:** Implement `MinimumHittingSet` as a new `src/models/set/` optimization model whose binary variables correspond to universe elements, not sets. Reuse the `MinimumSetCovering` registration and example-db patterns, but define `dims()` over `universe_size`, treat a configuration as valid iff every input set contains a selected element, and minimize the number of selected elements. Keep this PR model-only: no reduction rules or ILP reduction in this branch. - -**Tech Stack:** Rust workspace, `inventory` schema registry, `declare_variants!`, `BruteForce`, Typst paper, CLI `pred create` - ---- - -## Issue Checklist - -| Item | Value | -|---|---| -| Problem name | `MinimumHittingSet` | -| Category | `set` | -| Problem type | Optimization (`Direction::Minimize`) | -| Struct fields | `universe_size: usize`, `sets: Vec>` | -| Configuration space | `vec![2; universe_size]` | -| Feasibility rule | Every set in `sets` contains at least one selected universe element | -| Objective | Minimize the number of selected elements | -| Complexity string | `"2^universe_size"` | -| Solver for this PR | `BruteForce` only | -| Canonical example outcome | Universe `{0,1,2,3,4,5}`, sets `[{0,1,2},{0,3,4},{1,3,5},{2,4,5},{0,1,5},{2,3},{1,4}]`, optimal hitting set `{1,3,4}` with config `[0,1,0,1,1,0]` and value `3` | -| Associated open rules | `#200`, `#460`, `#462`, `#467` | - -## Design Notes - -- Use `SolutionSize` instead of a weight parameter. The issue explicitly models cardinality minimization, so a generic weight dimension would add unsupported surface area. -- Add `universe_size()` and `num_sets()` getters. `universe_size()` is required by the complexity string and `num_sets()` is useful for diagnostics/tests. -- Normalize each input set in the constructor by sorting/deduplicating it and assert that every element index is `< universe_size`. This prevents panics during evaluation on malformed instances. -- Treat empty input sets as making the instance unsatisfiable: every configuration should evaluate to `Invalid` if any set is empty. -- Do not add a short alias like `MHS`; the repo only adds short aliases that are standard in the literature. - -## Batch 1: Model, Registry, Tests, and CLI - -### Task 1: Add the failing model tests first - -**Files:** -- Create: `src/unit_tests/models/set/minimum_hitting_set.rs` -- Modify: `src/unit_tests/problem_size.rs` -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `src/lib.rs` (only if a missing re-export breaks test compilation) - -**Step 1: Write the failing tests** - -Add tests for: -- creation/accessors/dimensions -- valid vs invalid evaluation -- constructor normalization / out-of-range rejection -- brute-force optimum on the issue example -- serialization round-trip -- paper example consistency (`[0,1,0,1,1,0]` gives `Valid(3)`) -- `problem_size()` exposes `universe_size` and `num_sets` -- generic trait-consistency coverage includes `MinimumHittingSet` - -**Step 2: Run the targeted tests to verify RED** - -Run: - -```bash -cargo test minimum_hitting_set --lib -``` - -Expected: compile failure because `MinimumHittingSet` does not exist yet. - -**Step 3: Write the minimal implementation to satisfy the tests** - -Create `src/models/set/minimum_hitting_set.rs` with: -- `ProblemSchemaEntry` registration -- `MinimumHittingSet` struct with `new()`, `universe_size()`, `num_sets()`, `sets()`, `get_set()` -- helpers such as `selected_elements()` / `is_valid_solution()` as needed -- `Problem` impl with `Metric = SolutionSize`, `dims() = vec![2; universe_size]`, `evaluate()` -- `OptimizationProblem` impl with `Direction::Minimize` -- `declare_variants! { default opt MinimumHittingSet => "2^universe_size", }` -- `#[cfg(test)]` link to the new unit test file - -Mirror the style of `src/models/set/minimum_set_covering.rs`, but keep the semantics element-based rather than set-based. - -**Step 4: Run the targeted tests to verify GREEN** - -Run: - -```bash -cargo test minimum_hitting_set --lib -``` - -Expected: the new model tests pass. - -**Step 5: Commit** - -```bash -git add src/models/set/minimum_hitting_set.rs src/unit_tests/models/set/minimum_hitting_set.rs src/unit_tests/problem_size.rs src/unit_tests/trait_consistency.rs -git commit -m "Add MinimumHittingSet model core" -``` - -### Task 2: Register the model and canonical example data - -**Files:** -- Modify: `src/models/set/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/models/set/minimum_hitting_set.rs` - -**Step 1: Add the failing example/registration checks** - -Extend the new model tests so they exercise: -- crate-level/prelude imports if needed -- `canonical_model_example_specs()` contents for `MinimumHittingSet` -- brute-force optimal config/value for the canonical example - -**Step 2: Run the focused tests to verify RED** - -Run: - -```bash -cargo test minimum_hitting_set --features example-db --lib -``` - -Expected: failure until the model is exported and its canonical example is registered. - -**Step 3: Wire the model into the registry surface** - -- Add `pub(crate) mod minimum_hitting_set;` and `pub use minimum_hitting_set::MinimumHittingSet;` in `src/models/set/mod.rs` -- Extend the set example chain with `minimum_hitting_set::canonical_model_example_specs()` -- Re-export `MinimumHittingSet` from `src/models/mod.rs` and the crate prelude in `src/lib.rs` -- Add `#[cfg(feature = "example-db")] canonical_model_example_specs()` in the model file using the issue example and `optimal_value = {"Valid": 3}` - -**Step 4: Run the focused tests to verify GREEN** - -Run: - -```bash -cargo test minimum_hitting_set --features example-db --lib -``` - -Expected: the example-db aware tests pass. - -**Step 5: Commit** - -```bash -git add src/models/set/mod.rs src/models/mod.rs src/lib.rs src/models/set/minimum_hitting_set.rs -git commit -m "Register MinimumHittingSet in the model catalog" -``` - -### Task 3: Add CLI creation support and CLI tests - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` only if a lowercase full-name lookup test proves it is needed - -**Step 1: Write the failing CLI tests** - -Add CLI coverage for: -- `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"` -- optional `--example` flow if existing set-model tests cover that pattern -- help table text includes `MinimumHittingSet --universe, --sets` - -**Step 2: Run the targeted CLI tests to verify RED** - -Run: - -```bash -cargo test -p problemreductions-cli minimum_hitting_set -``` - -Expected: failure because CLI creation/help do not know the new model yet. - -**Step 3: Implement the minimal CLI support** - -- Add a `create()` match arm analogous to `MinimumSetCovering`, but call `MinimumHittingSet::new(universe, sets)` -- Update the create-help table in `problemreductions-cli/src/cli.rs` -- Only touch alias resolution if the tests show the registry does not already resolve the canonical name adequately - -**Step 4: Run the targeted CLI tests to verify GREEN** - -Run: - -```bash -cargo test -p problemreductions-cli minimum_hitting_set -``` - -Expected: the new CLI tests pass. - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs problemreductions-cli/tests/cli_tests.rs problemreductions-cli/src/problem_name.rs -git commit -m "Add MinimumHittingSet CLI creation support" -``` - -## Batch 2: Paper Entry - -### Task 4: Document the model in the paper after the implementation exists - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` only if the chosen citations are missing -- Modify: `src/unit_tests/models/set/minimum_hitting_set.rs` if the paper example wording requires tightening the paper-example assertions - -**Step 1: Write or tighten the failing paper-example test** - -Ensure the unit test enforces the issue example exactly: -- instance matches the paper example -- config `[0,1,0,1,1,0]` is valid -- brute-force confirms optimum value `3` - -**Step 2: Run the paper-example test to verify RED if needed** - -Run: - -```bash -cargo test test_minimum_hitting_set_paper_example --lib -``` - -Expected: fail only if the paper-facing example and code diverge. - -**Step 3: Add the paper entry** - -In `docs/paper/reductions.typ`: -- add `"MinimumHittingSet": [Minimum Hitting Set]` to `display-name` -- add a `problem-def("MinimumHittingSet")` entry near the other set problems -- cite `@karp1972` for historical context -- explain the duality with Set Covering and the Vertex Cover special case -- use the issue example with a set-system diagram and the optimal hitting set `{1,3,4}` - -Prefer the `MinimumSetCovering` entry as the layout template, but highlight selected universe elements instead of selected sets. - -**Step 4: Verify the paper build** - -Run: - -```bash -cargo test test_minimum_hitting_set_paper_example --lib -make paper -``` - -Expected: the paper example test passes and the Typst build succeeds. - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ docs/paper/references.bib src/unit_tests/models/set/minimum_hitting_set.rs -git commit -m "Document MinimumHittingSet in the paper" -``` - -## Final Verification - -After both batches are complete, run: - -```bash -make test -make clippy -git status --short -``` - -Expected: -- tests pass -- clippy passes -- only intended tracked files are modified - -## issue-to-pr Cleanup - -After implementation is committed and verified: - -1. Delete this file with `git rm docs/plans/2026-03-21-minimum-hitting-set.md` -2. Commit the removal: `git commit -m "chore: remove plan file after implementation"` -3. Post the PR implementation summary comment -4. Push the branch From 3ce5ad49bb68f5698f4649de72b9230bf4849c7c Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 15:17:13 +0800 Subject: [PATCH 8/8] Fix merge conflicts: alphabetical ordering in set/mod.rs and rustfmt Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 5 +++- src/lib.rs | 3 +-- src/models/set/minimum_hitting_set.rs | 21 ++++++++------- src/models/set/mod.rs | 6 ++--- .../models/set/minimum_hitting_set.rs | 26 +++++++++---------- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 1c13fe569..a5000e00a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1765,7 +1765,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } } } - (ser(MinimumHittingSet::new(universe, sets))?, resolved_variant.clone()) + ( + ser(MinimumHittingSet::new(universe, sets))?, + resolved_variant.clone(), + ) } // MinimumSetCovering diff --git a/src/lib.rs b/src/lib.rs index a414a44bf..232b55940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,8 +76,7 @@ pub mod prelude { }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, - MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, - SetBasis, + MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis, }; // Core traits diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 896c8d04e..06c362382 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -59,7 +59,10 @@ impl MinimumHittingSet { } } - Self { universe_size, sets } + Self { + universe_size, + sets, + } } /// Get the universe size. @@ -105,9 +108,10 @@ impl MinimumHittingSet { return false; }; - self.sets - .iter() - .all(|set| set.iter().any(|element| selected.binary_search(element).is_ok())) + self.sets.iter().all(|set| { + set.iter() + .any(|element| selected.binary_search(element).is_ok()) + }) } } @@ -124,11 +128,10 @@ impl Problem for MinimumHittingSet { return SolutionSize::Invalid; }; - if self - .sets - .iter() - .all(|set| set.iter().any(|element| selected.binary_search(element).is_ok())) - { + if self.sets.iter().all(|set| { + set.iter() + .any(|element| selected.binary_search(element).is_ok()) + }) { SolutionSize::Valid(selected.len()) } else { SolutionSize::Invalid diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index 21733a0d8..d96b2d3e2 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -13,8 +13,8 @@ pub(crate) mod comparative_containment; pub(crate) mod consecutive_sets; pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; -pub(crate) mod minimum_hitting_set; pub(crate) mod minimum_cardinality_key; +pub(crate) mod minimum_hitting_set; pub(crate) mod minimum_set_covering; pub(crate) mod prime_attribute_name; pub(crate) mod set_basis; @@ -24,8 +24,8 @@ pub use comparative_containment::ComparativeContainment; pub use consecutive_sets::ConsecutiveSets; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; -pub use minimum_hitting_set::MinimumHittingSet; pub use minimum_cardinality_key::MinimumCardinalityKey; +pub use minimum_hitting_set::MinimumHittingSet; pub use minimum_set_covering::MinimumSetCovering; pub use prime_attribute_name::PrimeAttributeName; pub use set_basis::SetBasis; @@ -38,9 +38,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec> = best_solutions.iter().cloned().collect(); assert!(best_solution_set.contains(&issue_example_config())); - assert!( - best_solutions - .iter() - .all(|config| problem.evaluate(config) == SolutionSize::Valid(3)) - ); + assert!(best_solutions + .iter() + .all(|config| problem.evaluate(config) == SolutionSize::Valid(3))); } #[test] @@ -110,7 +108,10 @@ fn test_minimum_hitting_set_serialization_round_trip() { fn test_minimum_hitting_set_paper_example_consistency() { let problem = issue_example_problem(); - assert_eq!(problem.evaluate(&issue_example_config()), SolutionSize::Valid(3)); + assert_eq!( + problem.evaluate(&issue_example_config()), + SolutionSize::Valid(3) + ); } #[test] @@ -121,12 +122,10 @@ fn test_minimum_hitting_set_direction() { #[test] fn test_minimum_hitting_set_declares_problem_size_fields() { - let fields: HashSet<&'static str> = - declared_size_fields("MinimumHittingSet").into_iter().collect(); - assert_eq!( - fields, - HashSet::from(["num_sets", "universe_size"]), - ); + let fields: HashSet<&'static str> = declared_size_fields("MinimumHittingSet") + .into_iter() + .collect(); + assert_eq!(fields, HashSet::from(["num_sets", "universe_size"]),); } #[cfg(feature = "example-db")] @@ -140,7 +139,8 @@ fn test_minimum_hitting_set_canonical_example_spec() { assert_eq!(spec.optimal_config, issue_example_config()); assert_eq!(spec.optimal_value, serde_json::json!({"Valid": 3})); - let problem: MinimumHittingSet = serde_json::from_value(spec.instance.serialize_json()).unwrap(); + let problem: MinimumHittingSet = + serde_json::from_value(spec.instance.serialize_json()).unwrap(); assert_eq!(problem.universe_size(), 6); assert_eq!(problem.sets().len(), 7);