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
126 changes: 126 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -11801,6 +11801,132 @@ the displayed rule, extracted from the corresponding `pred path` entry.
_Solution extraction._ For IS $I subset.eq V'$, return packing $cal(P) = {S_i : v_i in I}$ (same variable assignment).
]

// MinimumDominatingSet -> MinimumSetCovering
#let mds-sc = load-example("MinimumDominatingSet", "MinimumSetCovering")
#let mds-sc-sol = mds-sc.solutions.at(0)
#let mds-sc-source = mds-sc.source.instance
#let mds-sc-target = mds-sc.target.instance
#let mds-sc-n = graph-num-vertices(mds-sc-source)
#assert(mds-sc-n >= 3 and mds-sc-source.graph.edges == range(mds-sc-n - 1).map(v => (v, v + 1)))
#let mds-sc-indices = config => config.enumerate().filter(((v, selected)) => selected).map(((v, _)) => v)
#let mds-sc-cost = (config, weights) => mds-sc-indices(config).map(v => weights.at(v)).fold(0, (total, w) => total + w)
#let mds-sc-covered = config => range(mds-sc-target.universe_size).filter(u =>
mds-sc-target.sets.enumerate().any(((v, subset)) => config.at(v) and u in subset))
#let mds-sc-dominates = config => range(mds-sc-n).all(u => config.at(u) or
mds-sc-source.graph.edges.any(e => (e.at(0) == u and config.at(e.at(1))) or
(e.at(1) == u and config.at(e.at(0)))))
#let mds-sc-configs = range(calc.pow(2, mds-sc-n)).map(mask => range(mds-sc-n).map(v =>
calc.rem(calc.floor(mask / calc.pow(2, v)), 2) == 1))
#let mds-sc-optimum = mds-sc-cost(mds-sc-sol.target_config, mds-sc-target.weights)
#let mds-sc-bound = mds-sc-optimum - 1
#let mds-sc-no-candidates = mds-sc-configs.filter(config => mds-sc-cost(config, mds-sc-target.weights) <= mds-sc-bound)
#let mds-sc-alternatives = mds-sc-configs.filter(config =>
mds-sc-covered(config).len() == mds-sc-target.universe_size and
mds-sc-cost(config, mds-sc-target.weights) > mds-sc-optimum
).sorted(key: config => mds-sc-cost(config, mds-sc-target.weights)).slice(0, 2)
#assert(mds-sc-target.universe_size == mds-sc-n and mds-sc-target.sets.len() == mds-sc-n)
#assert(mds-sc-target.weights == mds-sc-source.weights)
#assert(mds-sc-target.sets.map(s => s.len()).fold(0, (total, size) => total + size) ==
mds-sc-n + 2 * graph-num-edges(mds-sc-source))
#assert(mds-sc-sol.source_config == mds-sc-sol.target_config and mds-sc-dominates(mds-sc-sol.source_config))
#assert(mds-sc-covered(mds-sc-sol.target_config).len() == mds-sc-target.universe_size)
#assert(mds-sc-cost(mds-sc-sol.source_config, mds-sc-source.weights) == mds-sc-optimum)
#assert(mds-sc-configs.filter(config => mds-sc-covered(config).len() == mds-sc-target.universe_size and
mds-sc-cost(config, mds-sc-target.weights) <= mds-sc-optimum) == (mds-sc-sol.target_config,))
#assert(mds-sc-target.weights.all(w => w > 0) and mds-sc-no-candidates.len() > 0)
#assert(mds-sc-no-candidates.all(config => not mds-sc-dominates(config) and
mds-sc-covered(config).len() < mds-sc-target.universe_size))
#assert(mds-sc-alternatives.len() == 2 and mds-sc-alternatives.all(config => mds-sc-dominates(config)))
#reduction-rule("MinimumDominatingSet", "MinimumSetCovering",
example: true,
example-caption: [Closed neighbourhoods of a weighted path],
extra: [
#pred-commands(
"pred create --example " + rule-spec(mds-sc) + " -o dominating.json",
"pred reduce dominating.json --via route.json -o bundle.json",
"pred solve bundle.json",
"pred evaluate dominating.json --config " + cli-config(mds-sc-sol.source_config),
)
*Source and construction.* The source has #mds-sc-n vertices, edges
#{mds-sc-source.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and
weights $(#fmt-values(mds-sc-source.weights))$. The universe has
#mds-sc-target.universe_size elements and the indexed family has
#mds-sc-target.sets.len() sets:
#table(
columns: (auto, 1fr, auto),
[Index], [Closed neighbourhood], [Weight],
..mds-sc-target.sets.enumerate().map(((v, subset)) => (
[#v], [${#fmt-values(subset)}$], [#mds-sc-target.weights.at(v)],
)).flatten(),
)
The total membership count is
#mds-sc-target.sets.map(s => s.len()).fold(0, (total, size) => total + size),
equal to $#mds-sc-n + 2 dot #graph-num-edges(mds-sc-source)$.

*YES and extraction.* Selecting sets with indices
${#fmt-values(mds-sc-indices(mds-sc-sol.target_config))}$ covers
${#fmt-values(mds-sc-covered(mds-sc-sol.target_config))}$ at cost
#mds-sc-optimum. The identical source vector selects vertices
${#fmt-values(mds-sc-indices(mds-sc-sol.source_config))}$ and dominates the graph
with cost #mds-sc-cost(mds-sc-sol.source_config, mds-sc-source.weights).
Enumeration of the fixture's configurations confirms this is the unique optimum.
The following feasible choices are suboptimal:
#for config in mds-sc-alternatives [
indices ${#fmt-values(mds-sc-indices(config))}$ cost
#mds-sc-cost(config, mds-sc-target.weights) on each side;
]

*NO at a bound.* For this same graph and target family, impose bound
#mds-sc-bound. All weights are positive. The only choices of cost at most
#mds-sc-bound are the #mds-sc-no-candidates.len() choices below:
#for config in mds-sc-no-candidates [
indices ${#fmt-values(mds-sc-indices(config))}$ have cost
#mds-sc-cost(config, mds-sc-target.weights), cover
${#fmt-values(mds-sc-covered(config))}$, and leave
${#fmt-values(range(mds-sc-target.universe_size).filter(u => u not in mds-sc-covered(config)))}$
uncovered and undominated;
]
Thus neither side admits a witness within the bound. Without the bound,
both remain feasible with optimum #mds-sc-optimum.
],
)[
The closed-neighbourhood construction @umass2018dominatingcover turns weighted
domination into weighted covering with exactly $|V|$ universe elements and
$|V|$ indexed sets. Garey--Johnson @garey1979 gives the underlying problem
definitions; copying weights extends the construction to signed costs.
][
_Construction._ Let $G=(V,E)$ be a simple undirected graph with
$V={0,...,n-1}$ and weights $w_v$ in `i64`. Set $U=V$ and, for each vertex
$v$, create $D_v={v} union {u : {u,v} in E}$ with weight $w_v$.
Keep every indexed set even when neighbourhoods coincide. Each member is in
$U$, and one weight accompanies each of the $n$ sets. Each edge contributes
two incidences, so total membership is $n+2|E|$ and storage is $O(n+|E|)$;
sorting the sets takes polynomial time.

_Correctness._ ($arrow.r.double$) Given a dominating vertex selection $S$,
every $u in V$ either lies in $S$, hence in $D_u$, or has a selected neighbour
$v$, hence lies in $D_v$. The corresponding indexed sets cover $U$.
($arrow.l.double$) Given a cover, every vertex $u$ belongs to some selected
$D_v$. Then either $u=v$ or ${u,v} in E$, so the same selected indices dominate
every source vertex. Costs agree term by term, giving a bijection of feasible
configurations and preserving the optimum and every optimal witness.

_Solution extraction._ Validate the target witness once and copy its Boolean
vector: set index $v$ becomes vertex index $v$. Completed minimum values map
identically. Infeasible and wrong-length witnesses are rejected; evaluation
errors propagate. Both models add selected weights in increasing index order,
so `i64` overflow occurs at the same step, including before later cancellation.
Construction itself only copies weights; no objective arithmetic is needed.

_Special cases._ The empty graph gives an empty universe and family with
cost zero. An isolated vertex gives a forced singleton, whatever its sign.
Disconnected graphs need no extra condition; zero or negative weights may
make redundant selections optimal. Equal neighbourhoods retain separate
indices and costs. Self-loops and repeated edges are rejected by the source
graph type. Every unbounded instance here is feasible by selecting all
vertices or sets, so a NO example necessarily specifies a cost bound.
]

#reduction-rule("MinimumVertexCover", "MinimumSetCovering")[
A vertex cover must "hit" every edge; set covering must "hit" every universe element. By making each edge a universe element and each vertex the set of its incident edges, the two covering conditions become identical. This is the canonical embedding of vertex cover as a special case of set covering.
][
Expand Down
8 changes: 8 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ @book{garey1979
year = {1979}
}

@misc{umass2018dominatingcover,
author = {{University of Massachusetts Amherst, COMPSCI 311}},
title = {Second Midterm Exam Solutions, Problem 8(a)},
year = {2018},
month = nov,
url = {https://people.cs.umass.edu/~marius/class/cs311-fa18/midterm2-sol.pdf}
}

@article{orlin1977,
author = {James B. Orlin},
title = {Contentment in Graph Theory: Covering Graphs with Cliques},
Expand Down
5 changes: 4 additions & 1 deletion problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6884,7 +6884,10 @@ fn test_inspect_excludes_non_witness_reductions() {
String::from_utf8_lossy(&inspect.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inspect.stdout).unwrap();
assert_eq!(json["reduces_to"], serde_json::json!(["ILP"]));
assert_eq!(
json["reduces_to"],
serde_json::json!(["ILP", "MinimumSetCovering"])
);

std::fs::remove_file(problem_file).unwrap();
}
Expand Down
2 changes: 1 addition & 1 deletion src/models/set/minimum_set_covering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ impl<W: Clone + Default> MinimumSetCovering<W> {
.unwrap_or_else(|error| panic!("{error}"))
}

fn try_with_weights(
pub(crate) fn try_with_weights(
universe_size: usize,
sets: Vec<Vec<usize>>,
weights: Vec<W>,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/minimumdominatingset_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl ReduceTo<ILP<bool>> for MinimumDominatingSet<SimpleGraph, i64> {
.collect();

let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
.map_err(Self::target_construction)?;
.map_err(<Self as ReduceTo<ILP<bool>>>::target_construction)?;

Ok(ReductionDSToILP { target })
}
Expand Down
87 changes: 87 additions & 0 deletions src/rules/minimumdominatingset_minimumsetcovering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Closed-neighborhood reduction from MinimumDominatingSet to MinimumSetCovering.

use crate::models::graph::MinimumDominatingSet;
use crate::models::set::MinimumSetCovering;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::SimpleGraph;

/// The target's set indices are the source's vertex indices.
#[derive(Debug, Clone)]
pub struct ReductionDSToSC {
target: MinimumSetCovering<i64>,
}

impl ReductionResult for ReductionDSToSC {
type Source = MinimumDominatingSet<SimpleGraph, i64>;
type Target = MinimumSetCovering<i64>;

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

fn extract_solution(
&self,
target_solution: &<Self::Target as crate::Problem>::Solution,
) -> crate::rules::ExtractionResult<<Self::Source as crate::Problem>::Solution> {
crate::rules::traits::validate_target_witness(
&self.target,
target_solution,
|value| value.0.is_some(),
"selected sets do not cover the universe",
)?;
Ok(target_solution.to_vec())
}
}

#[crate::aggregate_reduction(identity)]
impl crate::rules::AggregateReductionResult for ReductionDSToSC {}

#[crate::reduction(transform = exact {
universe_size = "num_vertices",
num_sets = "num_vertices",
})]
impl ReduceTo<MinimumSetCovering<i64>> for MinimumDominatingSet<SimpleGraph, i64> {
type Result = ReductionDSToSC;

fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
let sets = (0..self.num_vertices())
.map(|v| {
let mut neighborhood: Vec<_> = self.closed_neighborhood(v).into_iter().collect();
neighborhood.sort_unstable();
neighborhood
})
.collect();
let target = MinimumSetCovering::try_with_weights(
self.num_vertices(),
sets,
self.weights().to_vec(),
)
.map_err(<Self as ReduceTo<MinimumSetCovering<i64>>>::target_construction)?;
Ok(ReductionDSToSC { target })
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
vec![crate::example_db::specs::RuleExampleSpec {
id: "minimumdominatingset_to_minimumsetcovering",
build: || {
let source = MinimumDominatingSet::new(
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
vec![3, 1, 4, 1, 3],
);
let config = serde_json::json!([false, true, false, true, false]);
crate::example_db::specs::rule_example_with_witness::<_, MinimumSetCovering<i64>>(
source,
crate::export::SolutionPair {
source_config: config.clone(),
target_config: config,
},
)
},
}]
}

#[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 @@ -114,6 +114,7 @@ pub(crate) mod minimumcostmaximumflow_minimumcostcirculation;
pub(crate) mod minimumcoveringbycliques_minimumintersectiongraphbasis;
pub(crate) mod minimumdiscreteplanarinversekinematics_qubo;
mod minimumdominatingset_casts;
pub(crate) mod minimumdominatingset_minimumsetcovering;
mod minimumfeedbackvertexset_casts;
pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters;
pub(crate) mod minimummaximalmatching_maximumachromaticnumber;
Expand Down Expand Up @@ -485,6 +486,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumhittingset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumsetcovering::canonical_rule_example_specs());
specs.extend(minimumdominatingset_minimumsetcovering::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumweightandorgraph::canonical_rule_example_specs());
specs.extend(naesatisfiability_setsplitting::canonical_rule_example_specs());
specs.extend(
Expand Down
Loading
Loading