diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a412a3a29..eb801a074 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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. ][ diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs new file mode 100644 index 000000000..7f984aa67 --- /dev/null +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -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 { + target: MinimumFeedbackVertexSet, +} + +impl ReductionResult for ReductionVCToFVS +where + W: WeightElement + crate::variant::VariantParam, +{ + type Source = MinimumVertexCover; + type Target = MinimumFeedbackVertexSet; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction( + overhead = { + num_vertices = "num_vertices", + num_arcs = "2 * num_edges", + } +)] +impl ReduceTo> for MinimumVertexCover { + type Result = ReductionVCToFVS; + + 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 { + 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>( + 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; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4b55c9aae..44c288b4d 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -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; @@ -110,6 +111,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec MinimumVertexCover { + 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 = + ReduceTo::>::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 = + ReduceTo::>::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 = + ReduceTo::>::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 = + ReduceTo::>::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 = + serde_json::from_value(example.source.instance.clone()) + .expect("source example deserializes"); + let target: MinimumFeedbackVertexSet = + 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)); +}