Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
83 changes: 83 additions & 0 deletions src/rules/minimumdominatingset_minimumhittingset.rs
Original file line number Diff line number Diff line change
@@ -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<SimpleGraph, One> to MinimumHittingSet.
#[derive(Debug, Clone)]
pub struct ReductionDominatingSetToHittingSet {
target: MinimumHittingSet,
}

impl ReductionResult for ReductionDominatingSetToHittingSet {
type Source = MinimumDominatingSet<SimpleGraph, One>;
type Target = MinimumHittingSet;

fn target_problem(&self) -> &Self::Target {
&self.target
}

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
target_solution.to_vec()
}
}

#[reduction(
overhead = {
universe_size = "num_vertices",
num_sets = "num_vertices",
}
)]
impl ReduceTo<MinimumHittingSet> for MinimumDominatingSet<SimpleGraph, One> {
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<crate::example_db::specs::RuleExampleSpec> {
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;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -498,6 +499,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
minimumcoveringbycliques_minimumintersectiongraphbasis::canonical_rule_example_specs(),
);
specs.extend(minimumdiscreteplanarinversekinematics_qubo::canonical_rule_example_specs());
specs.extend(minimumdominatingset_minimumhittingset::canonical_rule_example_specs());
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
specs.extend(paintshop_qubo::canonical_rule_example_specs());
specs.extend(prizecollectingsteinerforest_steinertree::canonical_rule_example_specs());
Expand Down
138 changes: 138 additions & 0 deletions src/unit_tests/rules/minimumdominatingset_minimumhittingset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use super::*;
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
use crate::traits::Problem;

fn source(
num_vertices: usize,
edges: Vec<(usize, usize)>,
) -> MinimumDominatingSet<SimpleGraph, One> {
MinimumDominatingSet::new(
SimpleGraph::new(num_vertices, edges),
vec![One; num_vertices],
)
}

fn assert_config_equivalent(
problem: &MinimumDominatingSet<SimpleGraph, One>,
target: &MinimumHittingSet,
config: &[usize],
) {
let selected = config.iter().sum::<usize>();
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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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::<MinimumHittingSet>::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);
}
}
}
}