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_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]); + 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", + ); +}