Skip to content
Merged
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
22 changes: 22 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4252,6 +4252,28 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead
_Solution extraction._ For IS solution $S$, return $C = V backslash S$, i.e.\ flip each variable: $c_v = 1 - s_v$.
]

#let mvc_fvs = load-example("MinimumVertexCover", "MinimumFeedbackVertexSet")
#let mvc_fvs_sol = mvc_fvs.solutions.at(0)
#let mvc_fvs_cover = mvc_fvs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
#let mvc_fvs_fvs = mvc_fvs_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
#reduction-rule("MinimumVertexCover", "MinimumFeedbackVertexSet",
example: true,
example-caption: [7-vertex graph: each source edge becomes a directed 2-cycle],
extra: [
Source VC: $C = {#mvc_fvs_cover.map(str).join(", ")}$ (size #mvc_fvs_cover.len()) on a graph with $n = #graph-num-vertices(mvc_fvs.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_fvs.source.instance)$ edges \
Target FVS: $F = {#mvc_fvs_fvs.map(str).join(", ")}$ (size #mvc_fvs_fvs.len()) on a digraph with the same $n = #graph-num-vertices(mvc_fvs.target.instance)$ vertices and $|A| = #mvc_fvs.target.instance.graph.arcs.len() = 2 |E|$ arcs \
Canonical witness is preserved exactly: $C = F$ #sym.checkmark
],
)[
Each undirected edge $\{u, v\}$ can be viewed as the directed 2-cycle $u -> v -> u$. Replacing every source edge this way turns the task "hit every edge with a chosen endpoint" into "hit every directed cycle with a chosen vertex." The vertex set, weights, and budget are preserved, so the reduction is size-preserving up to doubling the edge count into arcs.
][
_Construction._ Given a Minimum Vertex Cover instance $(G = (V, E), bold(w))$, build the directed graph $D = (V, A)$ on the same vertex set, where for every undirected edge $\{u, v\} in E$ we add both arcs $(u, v)$ and $(v, u)$ to $A$. Keep the vertex weights unchanged and reuse the same decision variables $x_v in {0,1}$.

_Correctness._ ($arrow.r.double$) If $C subset.eq V$ is a vertex cover of $G$, then every source edge $\{u, v\}$ has an endpoint in $C$, so the corresponding 2-cycle $u -> v -> u$ in $D$ is hit by $C$. Any longer directed cycle in $D$ is also made from source edges, so one of its vertices lies in $C$ as well. Therefore removing $C$ destroys all directed cycles, and $C$ is a feedback vertex set of $D$. ($arrow.l.double$) If $F subset.eq V$ is a feedback vertex set of $D$, then for every source edge $\{u, v\}$ the digraph contains the 2-cycle $u -> v -> u$, which must be hit by $F$. Hence at least one of $u, v$ lies in $F$, so $F$ covers every edge of $G$ and is a vertex cover.

_Solution extraction._ Return the target solution vector unchanged: a selected vertex in the feedback vertex set is selected in the vertex cover, and vice versa.
]

#reduction-rule("MaximumIndependentSet", "MinimumVertexCover")[
The exact reverse of VC $arrow.r$ IS: complementing an independent set yields a vertex cover. The graph and weights are preserved unchanged, and $|"IS"| + |"VC"| = |V|$ ensures optimality carries over.
][
Expand Down
97 changes: 97 additions & 0 deletions src/rules/minimumvertexcover_minimumfeedbackvertexset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Reduction from MinimumVertexCover to MinimumFeedbackVertexSet.
//!
//! Each undirected edge becomes a directed 2-cycle, so a vertex cover is
//! exactly a feedback vertex set in the constructed digraph.

use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{DirectedGraph, Graph, SimpleGraph};
use crate::types::WeightElement;

/// Result of reducing MinimumVertexCover to MinimumFeedbackVertexSet.
#[derive(Debug, Clone)]
pub struct ReductionVCToFVS<W> {
target: MinimumFeedbackVertexSet<W>,
}

impl<W> ReductionResult for ReductionVCToFVS<W>
where
W: WeightElement + crate::variant::VariantParam,
{
type Source = MinimumVertexCover<SimpleGraph, W>;
type Target = MinimumFeedbackVertexSet<W>;

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

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

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_arcs = "2 * num_edges",
}
)]
impl ReduceTo<MinimumFeedbackVertexSet<i32>> for MinimumVertexCover<SimpleGraph, i32> {
type Result = ReductionVCToFVS<i32>;

fn reduce_to(&self) -> Self::Result {
let arcs = self
.graph()
.edges()
.into_iter()
.flat_map(|(u, v)| [(u, v), (v, u)])
.collect();

let target = MinimumFeedbackVertexSet::new(
DirectedGraph::new(self.graph().num_vertices(), arcs),
self.weights().to_vec(),
);

ReductionVCToFVS { 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: "minimumvertexcover_to_minimumfeedbackvertexset",
build: || {
let source = MinimumVertexCover::new(
SimpleGraph::new(
7,
vec![
(0, 1),
(0, 2),
(0, 3),
(1, 2),
(1, 3),
(3, 4),
(4, 5),
(5, 6),
],
),
vec![1i32; 7],
);

crate::example_db::specs::rule_example_with_witness::<_, MinimumFeedbackVertexSet<i32>>(
source,
SolutionPair {
source_config: vec![1, 1, 0, 1, 0, 1, 0],
target_config: vec![1, 1, 0, 1, 0, 1, 0],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.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 @@ -28,6 +28,7 @@ mod maximumsetpacking_casts;
pub(crate) mod maximumsetpacking_qubo;
pub(crate) mod minimummultiwaycut_qubo;
pub(crate) mod minimumvertexcover_maximumindependentset;
pub(crate) mod minimumvertexcover_minimumfeedbackvertexset;
pub(crate) mod minimumvertexcover_minimumsetcovering;
pub(crate) mod sat_circuitsat;
pub(crate) mod sat_coloring;
Expand Down Expand Up @@ -110,6 +111,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(maximumsetpacking_qubo::canonical_rule_example_specs());
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
specs.extend(minimumvertexcover_maximumindependentset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
specs.extend(minimumvertexcover_minimumsetcovering::canonical_rule_example_specs());
specs.extend(sat_circuitsat::canonical_rule_example_specs());
specs.extend(sat_coloring::canonical_rule_example_specs());
Expand Down
130 changes: 130 additions & 0 deletions src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#[cfg(feature = "example-db")]
use super::canonical_rule_example_specs;
use super::ReductionVCToFVS;
use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover};
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
use crate::rules::traits::ReductionResult;
use crate::rules::ReduceTo;
#[cfg(feature = "example-db")]
use crate::solvers::{BruteForce, Solver};
use crate::topology::{Graph, SimpleGraph};
#[cfg(feature = "example-db")]
use crate::traits::Problem;

fn weighted_cycle_cover_source() -> MinimumVertexCover<SimpleGraph, i32> {
MinimumVertexCover::new(
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)]),
vec![4, 1, 3, 2, 5],
)
}

#[test]
fn test_minimumvertexcover_to_minimumfeedbackvertexset_closed_loop() {
let source = weighted_cycle_cover_source();
let reduction: ReductionVCToFVS<i32> =
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);

assert_optimization_round_trip_from_optimization_target(
&source,
&reduction,
"MVC -> FVS closed loop",
);
}

#[test]
fn test_reduction_structure() {
let source = weighted_cycle_cover_source();
let reduction: ReductionVCToFVS<i32> =
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.graph().num_vertices(), source.graph().num_vertices());
assert_eq!(target.num_arcs(), 2 * source.num_edges());

let mut arcs = target.graph().arcs();
arcs.sort_unstable();

assert_eq!(
arcs,
vec![
(0, 1),
(0, 2),
(1, 0),
(1, 2),
(2, 0),
(2, 1),
(2, 3),
(3, 2),
(3, 4),
(4, 3),
]
);
}

#[test]
fn test_weight_preservation() {
let source = weighted_cycle_cover_source();
let reduction: ReductionVCToFVS<i32> =
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);

assert_eq!(reduction.target_problem().weights(), source.weights());
}

#[test]
fn test_identity_solution_extraction() {
let source = weighted_cycle_cover_source();
let reduction: ReductionVCToFVS<i32> =
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);

assert_eq!(
reduction.extract_solution(&[1, 0, 1, 0, 1]),
vec![1, 0, 1, 0, 1]
);
}

#[cfg(feature = "example-db")]
#[test]
fn test_canonical_rule_example_spec_builds() {
let example = (canonical_rule_example_specs()
.into_iter()
.find(|spec| spec.id == "minimumvertexcover_to_minimumfeedbackvertexset")
.expect("example spec should be registered")
.build)();

assert_eq!(example.source.problem, "MinimumVertexCover");
assert_eq!(example.target.problem, "MinimumFeedbackVertexSet");
assert_eq!(example.solutions.len(), 1);
assert_eq!(
example.solutions[0].source_config,
example.solutions[0].target_config
);

let source: MinimumVertexCover<SimpleGraph, i32> =
serde_json::from_value(example.source.instance.clone())
.expect("source example deserializes");
let target: MinimumFeedbackVertexSet<i32> =
serde_json::from_value(example.target.instance.clone())
.expect("target example deserializes");
let solution = &example.solutions[0];

let source_metric = source.evaluate(&solution.source_config);
let target_metric = target.evaluate(&solution.target_config);
assert!(
source_metric.is_valid(),
"source witness should be feasible"
);
assert!(
target_metric.is_valid(),
"target witness should be feasible"
);

let best_source = BruteForce::new()
.find_best(&source)
.expect("source example should have an optimum");
let best_target = BruteForce::new()
.find_best(&target)
.expect("target example should have an optimum");

assert_eq!(source_metric, source.evaluate(&best_source));
assert_eq!(target_metric, target.evaluate(&best_target));
}
Loading