diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 88cd07556..6cac0e2a4 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -14321,6 +14321,53 @@ The following reductions to Integer Linear Programming are straightforward formu _Remark._ Zero-weight edges are excluded because they allow degenerate optimal ILP solutions containing redundant cycles at no cost; following the convention of practical solvers (e.g., SCIP-Jack @kochmartin1998steiner), such edges should be contracted before applying the reduction. ] +#let mds_hs = load-example( + "MinimumDominatingSet", + "MinimumHittingSet", + source-variant: (graph: "SimpleGraph", weight: "One"), +) +#let mds_hs_sol = mds_hs.solutions.at(0) +#let mds_hs_graph = mds_hs.source.instance.graph +#let mds_hs_sets = mds_hs.target.instance.sets +#let mds_hs_dominators = mds_hs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => i) +#let mds_hs_hits = mds_hs_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => i) +#let mds_hs_incidences = mds_hs_sets.map(s => s.len()).sum() +#reduction-rule("MinimumDominatingSet", "MinimumHittingSet", + example: true, + example-source-variant: (graph: "SimpleGraph", weight: "One"), + example-caption: [Five-vertex path: closed neighborhoods form a Hitting Set instance], + extra: [ + #pred-commands( + "pred create --example " + problem-spec(mds_hs.source) + " -o dominating-set.json", + "pred reduce dominating-set.json --to " + target-spec(mds_hs) + " -o bundle.json", + "pred solve bundle.json", + "pred evaluate dominating-set.json --config " + mds_hs_sol.source_config.map(str).join(","), + ) + + *Step 1 -- Read the source path.* The fixture has $n = #mds_hs_graph.num_vertices$ vertices and $m = #mds_hs_graph.edges.len()$ edges, ordered as #mds_hs_graph.edges.map(((u, v)) => [$(#u, #v)$]).join(", "). Thus it is the path $0 dash 1 dash 2 dash 3 dash 4$, and its canonical dominating-set witness is $D = {#mds_hs_dominators.map(str).join(", ")}$. In indicator order this is $(#mds_hs_sol.source_config.map(str).join(", "))$. + + *Step 2 -- Replace vertices by closed neighborhoods.* Keep the vertex labels as the universe $U = {0, dots, #(mds_hs.target.instance.universe_size - 1)}$. For each vertex $v$, insert the set $N[v]$ consisting of $v$ and its neighbors. In source-vertex order the fixture yields #mds_hs_sets.enumerate().map(((v, s)) => [$N[#v] = {#s.map(str).join(", ")}$]).join(", "). + + *Step 3 -- Account for the exact overhead.* The target has $#mds_hs.target.instance.universe_size = n$ universe elements and $#mds_hs_sets.len() = n$ sets. Across those sets there are $#mds_hs_incidences = n + 2 m = #(mds_hs_graph.num_vertices + 2 * mds_hs_graph.edges.len())$ incidences: each vertex contributes itself once, and each undirected edge contributes its two endpoints to one another's closed neighborhoods. The construction therefore takes $O(n + m)$ time and space. + + *Step 4 -- Verify the round trip.* The target witness chooses $H = {#mds_hs_hits.map(str).join(", ")}$, with indicator vector $(#mds_hs_sol.target_config.map(str).join(", "))$. It intersects every displayed neighborhood: $1$ hits $N[0], N[1], N[2]$, while $3$ hits $N[2], N[3], N[4]$. Hence $H$ is a hitting set of size #mds_hs_hits.len(). Identity extraction returns the same coordinates, so the recovered source set is $D = H$ and dominates every path vertex #sym.checkmark + + *Multiplicity:* The fixture stores one canonical witness, not all optimal witnesses. Every minimum dominating set of this path is also a minimum hitting set under the same indicator vector, and conversely, because the construction and extraction preserve selected vertex labels exactly. + ], +)[ + The closed-neighborhood reduction noted by Bannach and Tantau @bannachTantau2018 maps a unit-weight graph $G = (V, E)$ to the Hitting Set instance whose universe is $V$ and whose sets are the closed neighborhoods $N[v]$. It is computable in $O(n + m)$ time and space and produces exactly $n$ universe elements, $n$ sets, and $n + 2m$ element-set incidences. +][ + _Construction._ Let $G = (V, E)$ be a simple undirected graph with $n = |V|$ vertices and $m = |E|$ edges. For $v in V$, define its closed neighborhood by + $ N[v] = {v} union {u in V : {u, v} in E}. $ + Construct the Minimum Hitting Set instance with universe $U = V$ and collection + $ cal(S) = {N[v] : v in V}. $ + A target coordinate $h_v$ represents the same selected vertex as the source coordinate $d_v$. Consequently, $|U| = n$, $|cal(S)| = n$, and $sum_(S in cal(S)) |S| = n + 2m$: every vertex supplies one self-incidence and every edge supplies two neighbor incidences. + + _Correctness._ ($arrow.r.double$) Let $D subset.eq V$ be a dominating set. For every $v in V$, either $v in D$ or a neighbor of $v$ lies in $D$. Equivalently, $D inter N[v] != emptyset$. Thus $D$ hits every set in $cal(S)$ and is a hitting set. ($arrow.l.double$) Let $H subset.eq U$ hit every set in $cal(S)$. For every $v in V$, $H inter N[v] != emptyset$, so either $v in H$ or some neighbor of $v$ lies in $H$. Hence $H$ dominates every vertex and is a dominating set. The correspondence preserves the selected vertex set and therefore its cardinality, so minimum solutions and optimal values coincide. + + _Solution extraction._ Return the target indicator vector unchanged: $d_v = h_v$ for every $v in V$. This identity extraction is valid for every target witness, not only the canonical fixture witness. +] + #let mvc_hs = load-example("MinimumVertexCover", "MinimumHittingSet") #let mvc_hs_sol = mvc_hs.solutions.at(0) #let mvc_hs_cover = mvc_hs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 186f70de2..f8653049e 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -2143,3 +2143,14 @@ @article{berlekampMcElieceTilborg1978 doi = {10.1109/TIT.1978.1055873} } +@inproceedings{bannachTantau2018, + author = {Max Bannach and Till Tantau}, + title = {Computing Hitting Set Kernels By {$AC^0$}-Circuits}, + booktitle = {35th Symposium on Theoretical Aspects of Computer Science (STACS 2018)}, + series = {Leibniz International Proceedings in Informatics (LIPIcs)}, + volume = {96}, + pages = {9:1--9:14}, + year = {2018}, + publisher = {Schloss Dagstuhl -- Leibniz-Zentrum f{\"u}r Informatik}, + doi = {10.4230/LIPIcs.STACS.2018.9} +} diff --git a/src/rules/minimumdominatingset_minimumhittingset.rs b/src/rules/minimumdominatingset_minimumhittingset.rs new file mode 100644 index 000000000..f4ba1871c --- /dev/null +++ b/src/rules/minimumdominatingset_minimumhittingset.rs @@ -0,0 +1,83 @@ +//! Reduction from unit-weight MinimumDominatingSet to MinimumHittingSet. +//! +//! Vertices become universe elements, and each vertex contributes its closed +//! neighborhood as a set. A dominating set is exactly a hitting set for this +//! collection. + +use crate::models::graph::MinimumDominatingSet; +use crate::models::set::MinimumHittingSet; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::{Graph, SimpleGraph}; +use crate::types::One; + +/// Result of reducing MinimumDominatingSet to MinimumHittingSet. +#[derive(Debug, Clone)] +pub struct ReductionDominatingSetToHittingSet { + target: MinimumHittingSet, +} + +impl ReductionResult for ReductionDominatingSetToHittingSet { + type Source = MinimumDominatingSet; + type Target = MinimumHittingSet; + + 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 = ReductionDominatingSetToHittingSet; + + fn reduce_to(&self) -> Self::Result { + let num_vertices = self.graph().num_vertices(); + let sets = (0..num_vertices) + .map(|vertex| { + let mut closed_neighborhood: Vec<_> = + self.closed_neighborhood(vertex).into_iter().collect(); + closed_neighborhood.sort_unstable(); + closed_neighborhood + }) + .collect(); + + ReductionDominatingSetToHittingSet { + target: MinimumHittingSet::new(num_vertices, sets), + } + } +} + +#[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_minimumhittingset", + build: || { + let source = MinimumDominatingSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![One; 5], + ); + crate::example_db::specs::rule_example_with_witness::<_, MinimumHittingSet>( + 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_minimumhittingset.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 95eb5f477..3d37abcfe 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_minimumhittingset; pub(crate) mod minimumfeedbackarcset_maximumlikelihoodranking; pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters; pub(crate) mod minimummaximalmatching_maximumachromaticnumber; @@ -498,6 +499,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, +) -> MinimumDominatingSet { + MinimumDominatingSet::new( + SimpleGraph::new(num_vertices, edges), + vec![One; num_vertices], + ) +} + +fn assert_config_equivalent( + problem: &MinimumDominatingSet, + target: &MinimumHittingSet, + config: &[usize], +) { + let selected = config.iter().sum::(); + let source_value = problem.evaluate(config).0.map(|value| value as usize); + let target_value = target.evaluate(config).0; + let expected = problem.is_valid_solution(config).then_some(selected); + + assert_eq!(source_value, expected); + assert_eq!(target_value, expected); +} + +#[test] +fn test_minimumdominatingset_to_minimumhittingset_closed_loop() { + let problem = source(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let reduction = ReduceTo::::reduce_to(&problem); + + assert_optimization_round_trip_from_optimization_target( + &problem, + &reduction, + "MinimumDominatingSet(One)->MinimumHittingSet closed loop", + ); +} + +#[test] +fn test_closed_neighborhood_structure() { + let problem = source(5, vec![(0, 2), (0, 1), (1, 3), (3, 4)]); + let reduction = ReduceTo::::reduce_to(&problem); + let target = reduction.target_problem(); + + assert_eq!(target.universe_size(), 5); + assert_eq!(target.num_sets(), 5); + assert_eq!( + target.sets(), + &[ + vec![0, 1, 2], + vec![0, 1, 3], + vec![0, 2], + vec![1, 3, 4], + vec![3, 4], + ] + ); +} + +#[test] +fn test_identity_extraction() { + let problem = source(3, vec![(0, 1), (1, 2)]); + let reduction = ReduceTo::::reduce_to(&problem); + + assert_eq!(reduction.extract_solution(&[0, 1, 0]), vec![0, 1, 0]); +} + +#[test] +fn test_empty_graph() { + let problem = source(0, vec![]); + let reduction = ReduceTo::::reduce_to(&problem); + + assert_eq!(reduction.target_problem().universe_size(), 0); + assert!(reduction.target_problem().sets().is_empty()); + assert_config_equivalent(&problem, reduction.target_problem(), &[]); +} + +#[test] +fn test_isolated_and_disconnected_vertices() { + let problem = source(5, vec![(0, 1), (2, 3)]); + let reduction = ReduceTo::::reduce_to(&problem); + + assert_eq!( + reduction.target_problem().sets(), + &[vec![0, 1], vec![0, 1], vec![2, 3], vec![2, 3], vec![4],] + ); + assert_config_equivalent(&problem, reduction.target_problem(), &[1, 0, 1, 0, 1]); +} + +#[test] +fn test_star_and_complete_graphs() { + let star = source(4, vec![(0, 1), (0, 2), (0, 3)]); + let star_reduction = ReduceTo::::reduce_to(&star); + assert_config_equivalent(&star, star_reduction.target_problem(), &[1, 0, 0, 0]); + + let complete = source(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let complete_reduction = ReduceTo::::reduce_to(&complete); + assert!(complete_reduction + .target_problem() + .sets() + .iter() + .all(|set| set == &vec![0, 1, 2, 3])); + assert_config_equivalent( + &complete, + complete_reduction.target_problem(), + &[0, 0, 1, 0], + ); +} + +#[test] +fn test_exhaustive_equivalence_through_four_vertices() { + for num_vertices in 0..=4 { + let possible_edges: Vec<_> = (0..num_vertices) + .flat_map(|u| ((u + 1)..num_vertices).map(move |v| (u, v))) + .collect(); + + for graph_mask in 0..(1_usize << possible_edges.len()) { + let edges = possible_edges + .iter() + .enumerate() + .filter_map(|(index, &edge)| ((graph_mask >> index) & 1 == 1).then_some(edge)) + .collect(); + let problem = source(num_vertices, edges); + let reduction = ReduceTo::::reduce_to(&problem); + + assert_eq!(reduction.target_problem().universe_size(), num_vertices); + assert_eq!(reduction.target_problem().num_sets(), num_vertices); + + for config_mask in 0..(1_usize << num_vertices) { + let config: Vec<_> = (0..num_vertices) + .map(|vertex| (config_mask >> vertex) & 1) + .collect(); + assert_config_equivalent(&problem, reduction.target_problem(), &config); + } + } + } +}