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
35 changes: 35 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -2142,4 +2151,3 @@ @article{berlekampMcElieceTilborg1978
year = {1978},
doi = {10.1109/TIT.1978.1055873}
}

79 changes: 79 additions & 0 deletions src/rules/minimumdominatingset_minimumsetcovering.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
}

impl ReductionResult for ReductionDominatingSetToSetCovering {
type Source = MinimumDominatingSet<SimpleGraph, i32>;
type Target = MinimumSetCovering<i32>;

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<MinimumSetCovering<i32>> for MinimumDominatingSet<SimpleGraph, i32> {
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<crate::example_db::specs::RuleExampleSpec> {
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<i32>>(
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;
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_minimumsetcovering;
pub(crate) mod minimumfeedbackarcset_maximumlikelihoodranking;
pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters;
pub(crate) mod minimummaximalmatching_maximumachromaticnumber;
Expand Down Expand Up @@ -539,6 +540,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(minimumvertexcover_maximumindependentset::canonical_rule_example_specs());
specs.extend(minimummaximalmatching_maximumachromaticnumber::canonical_rule_example_specs());
specs.extend(minimummaximalmatching_minimummatrixdomination::canonical_rule_example_specs());
specs.extend(minimumdominatingset_minimumsetcovering::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimummaximalmatching::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumfeedbackarcset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
Expand Down
5 changes: 5 additions & 0 deletions src/unit_tests/rules/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ fn test_find_dominated_rules_returns_known_set() {
"MaximumMatching {graph: \"SimpleGraph\", weight: \"i32\"}",
"ILP {variable: \"bool\"}",
),
// MinimumDominatingSet → MinimumSetCovering → ILP is better than direct MDS → ILP
(
"MinimumDominatingSet {graph: \"SimpleGraph\", weight: \"i32\"}",
"ILP {variable: \"bool\"}",
),
// ExactCoverBy3Sets → MaxSetPacking → ILP is better than direct ExactCoverBy3Sets → ILP
("ExactCoverBy3Sets", "ILP {variable: \"bool\"}"),
// GraphPartitioning → MaxCut → SpinGlass → QUBO is better than direct GraphPartitioning → QUBO
Expand Down
125 changes: 125 additions & 0 deletions src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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() {
let source = MinimumDominatingSet::new(SimpleGraph::path(5), vec![3, 1, 4, 1, 3]);
let reduction = ReduceTo::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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::<MinimumSetCovering<i32>>::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",
);
}