From 16e55b606ad124c85166e75bea473c4cd5a50c36 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 2 Aug 2026 08:15:04 +0800 Subject: [PATCH 1/4] Add plan for #1095: MinimumDominatingSet to MinimumSetCovering --- ...-dominating-set-to-minimum-set-covering.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md diff --git a/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md b/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md new file mode 100644 index 000000000..6d38692e4 --- /dev/null +++ b/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md @@ -0,0 +1,45 @@ +# Minimum Dominating Set to Minimum Set Covering + +Issue: #1095 + +Base: `a9067297` + +Implement the witness-preserving reduction +`MinimumDominatingSet -> MinimumSetCovering` by following +the repository's `add-rule` skill. + +## Batch 1: Verify and implement + +1. Follow `verify-reduction` before writing Rust. Prove that selecting vertices + is equivalent to selecting their closed-neighborhood sets, verify the exact + overhead, and independently test at least 5,000 instances/checks in both the + constructor and adversary implementations. Include the weighted path + `0-1-2-3-4`, an empty graph, isolated vertices, signed weights, self-loops, + and repeated edges. +2. Add `src/rules/minimumdominatingset_minimumsetcovering.rs`. Construct universe + `V`, one deduplicated set `N[v]` per vertex, and copy vertex weights. Extract + the source witness coordinate-for-coordinate. Register exact overhead + `universe_size = num_vertices` and `num_sets = num_vertices`. +3. Register the module directly in `src/rules/mod.rs`. +4. Add focused tests in + `src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs` for the + canonical weighted closed loop, exact target structure, signed-weight + optimality/extraction, empty and isolated graphs, and deduplication of + self-loops/repeated edges. +5. Add the canonical five-vertex weighted path to the rule's example-db specs, + using source witness `[0, 1, 0, 1, 0]` and the identical target witness. +6. Run focused tests, formatting, and clippy before handing off to Batch 2. + +## Batch 2: Paper, fixtures, and final verification + +1. Add a self-contained `reduction-rule("MinimumDominatingSet", + "MinimumSetCovering", ...)` entry to `docs/paper/reductions.typ`, adapting + the verified construction and proof rather than rewriting it. Include the + canonical example via loaded fixture data and a `pred-commands()` block. +2. Cite Garey--Johnson for the problem definitions and the UMass COMPSCI 311 + solution for the explicit closed-neighborhood reduction. +3. Regenerate the reduction graph, schemas, and example fixtures. Stage only + tracked artifacts required by this rule. +4. Run `make paper`, `make test`, `make clippy`, and the relevant coverage check. + Confirm the tree is clean except for the temporary plan file, which the + pipeline removes after implementation. From 4b3b2a742e14d2ccdf5c0c7dfa53178ff4acf438 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 2 Aug 2026 08:44:11 +0800 Subject: [PATCH 2/4] Implement #1095: reduce dominating set to set covering --- docs/paper/reductions.typ | 35 ++++++ docs/paper/references.bib | 10 +- ...minimumdominatingset_minimumsetcovering.rs | 79 +++++++++++++ src/rules/mod.rs | 2 + src/unit_tests/rules/analysis.rs | 5 + ...minimumdominatingset_minimumsetcovering.rs | 110 ++++++++++++++++++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/rules/minimumdominatingset_minimumsetcovering.rs create mode 100644 src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 88cd07556..a75f55cee 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11940,6 +11940,41 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead _Solution extraction._ For covering ${S_v : v in C}$, return VC $= C$ (same variable assignment). ] +#let mds_msc = load-example("MinimumDominatingSet", "MinimumSetCovering") +#let mds_msc_sol = mds_msc.solutions.at(0) +#reduction-rule("MinimumDominatingSet", "MinimumSetCovering", + example: true, + example-caption: [Weighted path $P_5$: closed neighborhoods form a set covering instance], + extra: [ + #pred-commands( + "pred create --example " + problem-spec(mds_msc.source) + " -o dominating-set.json", + "pred reduce dominating-set.json --to " + target-spec(mds_msc) + " -o bundle.json", + "pred solve bundle.json", + "pred evaluate dominating-set.json --config " + mds_msc_sol.source_config.map(str).join(","), + ) + + *Step 1 -- Read the source graph.* The fixture is the path on $#graph-num-vertices(mds_msc.source.instance)$ vertices with edges #{mds_msc.source.instance.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")} and vertex weights $(#mds_msc.source.instance.weights.map(str).join(", "))$. + + *Step 2 -- Form the closed-neighborhood sets.* The target universe has $#mds_msc.target.instance.universe_size$ elements. In vertex order, its $#mds_msc.target.instance.sets.len()$ closed-neighborhood sets are #mds_msc.target.instance.sets.map(subset => "{" + subset.map(str).join(", ") + "}").join(", "), with weights $(#mds_msc.target.instance.weights.map(str).join(", "))$. Thus both target dimensions equal the source's $#graph-num-vertices(mds_msc.source.instance)$ vertices. + + *Step 3 -- Verify the canonical witness.* The source configuration $(#mds_msc_sol.source_config.map(str).join(", "))$ selects vertices ${#mds_msc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$, and the identical target configuration selects the corresponding closed-neighborhood sets. Their union is the entire universe, and their copied weights sum to $#mds_msc_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => mds_msc.target.instance.weights.at(i)).sum()$. + + *Multiplicity:* The fixture stores one canonical witness. Because vertex $v$ and set $D_v$ share the same coordinate, the reduction gives a bijection between all source configurations and target configurations, preserving feasibility and weight. + ], +)[ + Using the Garey--Johnson definitions of Dominating Set and Set Covering @garey1979, this closed-neighborhood reduction follows the explicit construction in the UMass COMPSCI 311 solution @umassCompsci3112018. Given a weighted graph $G = (V, E)$, it creates universe $U = V$ and one set $D_v = N[v]$ per vertex, with the same weight. The implementation runs in $O(|V| + |E| + sum_(v in V) deg(v) log deg(v))$ time because each deduplicated neighborhood is sorted. +][ + _Construction._ Let $G = (V, E)$ have vertices $V = {0, dots, n - 1}$ and weights $w: V -> ZZ$. Set the target universe to $U = V$. For every $v in V$, create the closed-neighborhood set + $ D_v = N[v] = {v} union {u in V : {u, v} in E}, $ + and assign it weight $w'(D_v) = w(v)$. Self-loops and repeated edges do not create repeated elements because $D_v$ is a set. The target contains exactly $n$ universe elements and $n$ sets. + + _Correctness._ ($arrow.r.double$) If $S subset.eq V$ dominates $G$, then every $u in V$ is either selected itself or adjacent to some selected $v in S$. Hence $u in D_v$ for some $v in S$, so ${D_v : v in S}$ covers $U$. ($arrow.l.double$) If ${D_v : v in S}$ covers $U$, then every $u in V$ belongs to some selected $D_v = N[v]$. Therefore $u = v$ or ${u, v} in E$, so $S$ dominates $G$. In both directions, + $ sum_(v in S) w(v) = sum_(v in S) w'(D_v), $ + so the correspondence preserves objective values, including signed weights, and therefore preserves optimality. + + _Solution extraction._ Return the target indicator vector unchanged: selecting $D_v$ maps to selecting vertex $v$ in the same coordinate. +] + #let dmvc_cc = load-example("DecisionMinimumVertexCover", "ComparativeContainment") #let dmvc_cc_sol = dmvc_cc.solutions.at(0) #reduction-rule("DecisionMinimumVertexCover", "ComparativeContainment", diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 186f70de2..bda2359c9 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -253,6 +253,15 @@ @book{garey1979 year = {1979} } +@misc{umassCompsci3112018, + author = {{University of Massachusetts Amherst}}, + title = {{COMPSCI 311: Introduction to Algorithms, Second Midterm Exam Solutions}}, + year = {2018}, + howpublished = {Course materials}, + url = {https://people.cs.umass.edu/~marius/class/cs311-fa18/midterm2-sol.pdf}, + note = {Question 8: reduction from Dominating Set to Set Cover} +} + @article{orlin1977, author = {James B. Orlin}, title = {Contentment in Graph Theory: Covering Graphs with Cliques}, @@ -2142,4 +2151,3 @@ @article{berlekampMcElieceTilborg1978 year = {1978}, doi = {10.1109/TIT.1978.1055873} } - diff --git a/src/rules/minimumdominatingset_minimumsetcovering.rs b/src/rules/minimumdominatingset_minimumsetcovering.rs new file mode 100644 index 000000000..1674bbf4a --- /dev/null +++ b/src/rules/minimumdominatingset_minimumsetcovering.rs @@ -0,0 +1,79 @@ +//! Reduction from MinimumDominatingSet to MinimumSetCovering. +//! +//! Each vertex becomes the set containing its closed neighborhood. + +use crate::models::graph::MinimumDominatingSet; +use crate::models::set::MinimumSetCovering; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::{Graph, SimpleGraph}; + +/// Result of reducing MinimumDominatingSet to MinimumSetCovering. +#[derive(Debug, Clone)] +pub struct ReductionDominatingSetToSetCovering { + target: MinimumSetCovering, +} + +impl ReductionResult for ReductionDominatingSetToSetCovering { + type Source = MinimumDominatingSet; + type Target = MinimumSetCovering; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction( + overhead = { + universe_size = "num_vertices", + num_sets = "num_vertices", + } +)] +impl ReduceTo> for MinimumDominatingSet { + type Result = ReductionDominatingSetToSetCovering; + + fn reduce_to(&self) -> Self::Result { + let sets = (0..self.graph().num_vertices()) + .map(|vertex| { + let mut closed_neighborhood: Vec<_> = + self.closed_neighborhood(vertex).into_iter().collect(); + closed_neighborhood.sort_unstable(); + closed_neighborhood + }) + .collect(); + let target = MinimumSetCovering::with_weights( + self.graph().num_vertices(), + sets, + self.weights().to_vec(), + ); + + ReductionDominatingSetToSetCovering { target } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "minimumdominatingset_to_minimumsetcovering", + build: || { + let source = MinimumDominatingSet::new(SimpleGraph::path(5), vec![3, 1, 4, 1, 3]); + crate::example_db::specs::rule_example_with_witness::<_, MinimumSetCovering>( + source, + SolutionPair { + source_config: vec![0, 1, 0, 1, 0], + target_config: vec![0, 1, 0, 1, 0], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/minimumdominatingset_minimumsetcovering.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 95eb5f477..f3221a642 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -92,6 +92,7 @@ pub(crate) mod maximumsetpacking_qubo; pub(crate) mod minimumcostmaximumflow_minimumcostcirculation; pub(crate) mod minimumcoveringbycliques_minimumintersectiongraphbasis; pub(crate) mod minimumdiscreteplanarinversekinematics_qubo; +pub(crate) mod minimumdominatingset_minimumsetcovering; pub(crate) mod minimumfeedbackarcset_maximumlikelihoodranking; pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters; pub(crate) mod minimummaximalmatching_maximumachromaticnumber; @@ -539,6 +540,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source); + + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "MinimumDominatingSet -> MinimumSetCovering weighted path", + ); + + let target_witnesses = BruteForce::new().find_all_witnesses(reduction.target_problem()); + assert_eq!(target_witnesses, vec![vec![0, 1, 0, 1, 0]]); + assert_eq!( + reduction.extract_solution(&target_witnesses[0]), + vec![0, 1, 0, 1, 0] + ); +} + +#[test] +fn test_exact_target_structure() { + let source = MinimumDominatingSet::new(SimpleGraph::path(5), vec![3, 1, 4, 1, 3]); + let reduction = ReduceTo::>::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.universe_size(), 5); + assert_eq!(target.num_sets(), 5); + assert_eq!( + target.sets(), + &[ + vec![0, 1], + vec![0, 1, 2], + vec![1, 2, 3], + vec![2, 3, 4], + vec![3, 4], + ] + ); + assert_eq!(target.weights_ref(), &[3, 1, 4, 1, 3]); +} + +#[test] +fn test_signed_weight_optimality_and_extraction() { + let source = MinimumDominatingSet::new(SimpleGraph::path(3), vec![-5, 10, -7]); + let reduction = ReduceTo::>::reduce_to(&source); + + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "MinimumDominatingSet -> MinimumSetCovering signed weights", + ); + + let target_witnesses = BruteForce::new().find_all_witnesses(reduction.target_problem()); + assert_eq!(target_witnesses, vec![vec![1, 0, 1]]); + assert_eq!( + reduction.extract_solution(&target_witnesses[0]), + vec![1, 0, 1] + ); +} + +#[test] +fn test_empty_and_isolated_graphs() { + let empty = MinimumDominatingSet::new(SimpleGraph::empty(0), vec![]); + let empty_reduction = ReduceTo::>::reduce_to(&empty); + assert_eq!(empty_reduction.target_problem().universe_size(), 0); + assert!(empty_reduction.target_problem().sets().is_empty()); + assert_optimization_round_trip_from_optimization_target( + &empty, + &empty_reduction, + "empty MinimumDominatingSet", + ); + + let isolated = MinimumDominatingSet::new(SimpleGraph::empty(3), vec![3, 2, 1]); + let isolated_reduction = ReduceTo::>::reduce_to(&isolated); + assert_eq!( + isolated_reduction.target_problem().sets(), + &[vec![0], vec![1], vec![2]] + ); + assert_eq!( + BruteForce::new().find_all_witnesses(isolated_reduction.target_problem()), + vec![vec![1, 1, 1]] + ); + assert_optimization_round_trip_from_optimization_target( + &isolated, + &isolated_reduction, + "isolated MinimumDominatingSet", + ); +} + +#[test] +fn test_self_loops_and_repeated_edges_are_deduplicated() { + let source = MinimumDominatingSet::new( + SimpleGraph::new(4, vec![(0, 0), (0, 1), (0, 1), (1, 0), (2, 2)]), + vec![-4, 2, -1, 7], + ); + let reduction = ReduceTo::>::reduce_to(&source); + + assert_eq!( + reduction.target_problem().sets(), + &[vec![0, 1], vec![0, 1], vec![2], vec![3]] + ); + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "MinimumDominatingSet with loops and repeated edges", + ); +} From a6544a4e2b8cb9e5e77a4ad7f658c4ad20eaee96 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 2 Aug 2026 08:44:11 +0800 Subject: [PATCH 3/4] chore: remove plan file after implementation --- ...-dominating-set-to-minimum-set-covering.md | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md diff --git a/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md b/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md deleted file mode 100644 index 6d38692e4..000000000 --- a/docs/plans/2026-08-02-minimum-dominating-set-to-minimum-set-covering.md +++ /dev/null @@ -1,45 +0,0 @@ -# Minimum Dominating Set to Minimum Set Covering - -Issue: #1095 - -Base: `a9067297` - -Implement the witness-preserving reduction -`MinimumDominatingSet -> MinimumSetCovering` by following -the repository's `add-rule` skill. - -## Batch 1: Verify and implement - -1. Follow `verify-reduction` before writing Rust. Prove that selecting vertices - is equivalent to selecting their closed-neighborhood sets, verify the exact - overhead, and independently test at least 5,000 instances/checks in both the - constructor and adversary implementations. Include the weighted path - `0-1-2-3-4`, an empty graph, isolated vertices, signed weights, self-loops, - and repeated edges. -2. Add `src/rules/minimumdominatingset_minimumsetcovering.rs`. Construct universe - `V`, one deduplicated set `N[v]` per vertex, and copy vertex weights. Extract - the source witness coordinate-for-coordinate. Register exact overhead - `universe_size = num_vertices` and `num_sets = num_vertices`. -3. Register the module directly in `src/rules/mod.rs`. -4. Add focused tests in - `src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs` for the - canonical weighted closed loop, exact target structure, signed-weight - optimality/extraction, empty and isolated graphs, and deduplication of - self-loops/repeated edges. -5. Add the canonical five-vertex weighted path to the rule's example-db specs, - using source witness `[0, 1, 0, 1, 0]` and the identical target witness. -6. Run focused tests, formatting, and clippy before handing off to Batch 2. - -## Batch 2: Paper, fixtures, and final verification - -1. Add a self-contained `reduction-rule("MinimumDominatingSet", - "MinimumSetCovering", ...)` entry to `docs/paper/reductions.typ`, adapting - the verified construction and proof rather than rewriting it. Include the - canonical example via loaded fixture data and a `pred-commands()` block. -2. Cite Garey--Johnson for the problem definitions and the UMass COMPSCI 311 - solution for the explicit closed-neighborhood reduction. -3. Regenerate the reduction graph, schemas, and example fixtures. Stage only - tracked artifacts required by this rule. -4. Run `make paper`, `make test`, `make clippy`, and the relevant coverage check. - Confirm the tree is clean except for the temporary plan file, which the - pipeline removes after implementation. From d443ae6342ef03e87e1b52b35db7137382b94cab Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 2 Aug 2026 13:36:02 +0800 Subject: [PATCH 4/4] fix: address PR #1104 review comments - test preservation of an infeasible endpoint-only configuration --- .../minimumdominatingset_minimumsetcovering.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs b/src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs index 43c22cd85..3ab7147a8 100644 --- a/src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs +++ b/src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs @@ -1,6 +1,8 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_minimumdominatingset_to_minimumsetcovering_closed_loop() { @@ -42,6 +44,19 @@ fn test_exact_target_structure() { assert_eq!(target.weights_ref(), &[3, 1, 4, 1, 3]); } +#[test] +fn test_infeasible_configuration_preservation() { + let source = MinimumDominatingSet::new(SimpleGraph::path(5), vec![3, 1, 4, 1, 3]); + let reduction = ReduceTo::>::reduce_to(&source); + let endpoint_only = vec![1, 0, 0, 0, 0]; + + assert_eq!(source.evaluate(&endpoint_only), Min(None)); + assert_eq!( + reduction.target_problem().evaluate(&endpoint_only), + Min(None) + ); +} + #[test] fn test_signed_weight_optimality_and_extraction() { let source = MinimumDominatingSet::new(SimpleGraph::path(3), vec![-5, 10, -7]);