Skip to content

Commit 1d7128b

Browse files
authored
Fix #207: [Rule] MinimumVertexCover to MinimumFeedbackVertexSet (#713)
* Add plan for #207: MinimumVertexCover to MinimumFeedbackVertexSet * Implement #207: add MinimumVertexCover to MinimumFeedbackVertexSet reduction * chore: remove plan file after implementation
1 parent d111c55 commit 1d7128b

4 files changed

Lines changed: 251 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4252,6 +4252,28 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead
42524252
_Solution extraction._ For IS solution $S$, return $C = V backslash S$, i.e.\ flip each variable: $c_v = 1 - s_v$.
42534253
]
42544254

4255+
#let mvc_fvs = load-example("MinimumVertexCover", "MinimumFeedbackVertexSet")
4256+
#let mvc_fvs_sol = mvc_fvs.solutions.at(0)
4257+
#let mvc_fvs_cover = mvc_fvs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
4258+
#let mvc_fvs_fvs = mvc_fvs_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i)
4259+
#reduction-rule("MinimumVertexCover", "MinimumFeedbackVertexSet",
4260+
example: true,
4261+
example-caption: [7-vertex graph: each source edge becomes a directed 2-cycle],
4262+
extra: [
4263+
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 \
4264+
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 \
4265+
Canonical witness is preserved exactly: $C = F$ #sym.checkmark
4266+
],
4267+
)[
4268+
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.
4269+
][
4270+
_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}$.
4271+
4272+
_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.
4273+
4274+
_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.
4275+
]
4276+
42554277
#reduction-rule("MaximumIndependentSet", "MinimumVertexCover")[
42564278
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.
42574279
][
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//! Reduction from MinimumVertexCover to MinimumFeedbackVertexSet.
2+
//!
3+
//! Each undirected edge becomes a directed 2-cycle, so a vertex cover is
4+
//! exactly a feedback vertex set in the constructed digraph.
5+
6+
use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover};
7+
use crate::reduction;
8+
use crate::rules::traits::{ReduceTo, ReductionResult};
9+
use crate::topology::{DirectedGraph, Graph, SimpleGraph};
10+
use crate::types::WeightElement;
11+
12+
/// Result of reducing MinimumVertexCover to MinimumFeedbackVertexSet.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionVCToFVS<W> {
15+
target: MinimumFeedbackVertexSet<W>,
16+
}
17+
18+
impl<W> ReductionResult for ReductionVCToFVS<W>
19+
where
20+
W: WeightElement + crate::variant::VariantParam,
21+
{
22+
type Source = MinimumVertexCover<SimpleGraph, W>;
23+
type Target = MinimumFeedbackVertexSet<W>;
24+
25+
fn target_problem(&self) -> &Self::Target {
26+
&self.target
27+
}
28+
29+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
30+
target_solution.to_vec()
31+
}
32+
}
33+
34+
#[reduction(
35+
overhead = {
36+
num_vertices = "num_vertices",
37+
num_arcs = "2 * num_edges",
38+
}
39+
)]
40+
impl ReduceTo<MinimumFeedbackVertexSet<i32>> for MinimumVertexCover<SimpleGraph, i32> {
41+
type Result = ReductionVCToFVS<i32>;
42+
43+
fn reduce_to(&self) -> Self::Result {
44+
let arcs = self
45+
.graph()
46+
.edges()
47+
.into_iter()
48+
.flat_map(|(u, v)| [(u, v), (v, u)])
49+
.collect();
50+
51+
let target = MinimumFeedbackVertexSet::new(
52+
DirectedGraph::new(self.graph().num_vertices(), arcs),
53+
self.weights().to_vec(),
54+
);
55+
56+
ReductionVCToFVS { target }
57+
}
58+
}
59+
60+
#[cfg(feature = "example-db")]
61+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
62+
use crate::export::SolutionPair;
63+
64+
vec![crate::example_db::specs::RuleExampleSpec {
65+
id: "minimumvertexcover_to_minimumfeedbackvertexset",
66+
build: || {
67+
let source = MinimumVertexCover::new(
68+
SimpleGraph::new(
69+
7,
70+
vec![
71+
(0, 1),
72+
(0, 2),
73+
(0, 3),
74+
(1, 2),
75+
(1, 3),
76+
(3, 4),
77+
(4, 5),
78+
(5, 6),
79+
],
80+
),
81+
vec![1i32; 7],
82+
);
83+
84+
crate::example_db::specs::rule_example_with_witness::<_, MinimumFeedbackVertexSet<i32>>(
85+
source,
86+
SolutionPair {
87+
source_config: vec![1, 1, 0, 1, 0, 1, 0],
88+
target_config: vec![1, 1, 0, 1, 0, 1, 0],
89+
},
90+
)
91+
},
92+
}]
93+
}
94+
95+
#[cfg(test)]
96+
#[path = "../unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs"]
97+
mod tests;

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod maximumsetpacking_casts;
2929
pub(crate) mod maximumsetpacking_qubo;
3030
pub(crate) mod minimummultiwaycut_qubo;
3131
pub(crate) mod minimumvertexcover_maximumindependentset;
32+
pub(crate) mod minimumvertexcover_minimumfeedbackvertexset;
3233
pub(crate) mod minimumvertexcover_minimumsetcovering;
3334
pub(crate) mod sat_circuitsat;
3435
pub(crate) mod sat_coloring;
@@ -112,6 +113,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
112113
specs.extend(maximumsetpacking_qubo::canonical_rule_example_specs());
113114
specs.extend(minimummultiwaycut_qubo::canonical_rule_example_specs());
114115
specs.extend(minimumvertexcover_maximumindependentset::canonical_rule_example_specs());
116+
specs.extend(minimumvertexcover_minimumfeedbackvertexset::canonical_rule_example_specs());
115117
specs.extend(minimumvertexcover_minimumsetcovering::canonical_rule_example_specs());
116118
specs.extend(sat_circuitsat::canonical_rule_example_specs());
117119
specs.extend(sat_coloring::canonical_rule_example_specs());
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#[cfg(feature = "example-db")]
2+
use super::canonical_rule_example_specs;
3+
use super::ReductionVCToFVS;
4+
use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover};
5+
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
6+
use crate::rules::traits::ReductionResult;
7+
use crate::rules::ReduceTo;
8+
#[cfg(feature = "example-db")]
9+
use crate::solvers::{BruteForce, Solver};
10+
use crate::topology::{Graph, SimpleGraph};
11+
#[cfg(feature = "example-db")]
12+
use crate::traits::Problem;
13+
14+
fn weighted_cycle_cover_source() -> MinimumVertexCover<SimpleGraph, i32> {
15+
MinimumVertexCover::new(
16+
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)]),
17+
vec![4, 1, 3, 2, 5],
18+
)
19+
}
20+
21+
#[test]
22+
fn test_minimumvertexcover_to_minimumfeedbackvertexset_closed_loop() {
23+
let source = weighted_cycle_cover_source();
24+
let reduction: ReductionVCToFVS<i32> =
25+
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);
26+
27+
assert_optimization_round_trip_from_optimization_target(
28+
&source,
29+
&reduction,
30+
"MVC -> FVS closed loop",
31+
);
32+
}
33+
34+
#[test]
35+
fn test_reduction_structure() {
36+
let source = weighted_cycle_cover_source();
37+
let reduction: ReductionVCToFVS<i32> =
38+
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);
39+
let target = reduction.target_problem();
40+
41+
assert_eq!(target.graph().num_vertices(), source.graph().num_vertices());
42+
assert_eq!(target.num_arcs(), 2 * source.num_edges());
43+
44+
let mut arcs = target.graph().arcs();
45+
arcs.sort_unstable();
46+
47+
assert_eq!(
48+
arcs,
49+
vec![
50+
(0, 1),
51+
(0, 2),
52+
(1, 0),
53+
(1, 2),
54+
(2, 0),
55+
(2, 1),
56+
(2, 3),
57+
(3, 2),
58+
(3, 4),
59+
(4, 3),
60+
]
61+
);
62+
}
63+
64+
#[test]
65+
fn test_weight_preservation() {
66+
let source = weighted_cycle_cover_source();
67+
let reduction: ReductionVCToFVS<i32> =
68+
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);
69+
70+
assert_eq!(reduction.target_problem().weights(), source.weights());
71+
}
72+
73+
#[test]
74+
fn test_identity_solution_extraction() {
75+
let source = weighted_cycle_cover_source();
76+
let reduction: ReductionVCToFVS<i32> =
77+
ReduceTo::<MinimumFeedbackVertexSet<i32>>::reduce_to(&source);
78+
79+
assert_eq!(
80+
reduction.extract_solution(&[1, 0, 1, 0, 1]),
81+
vec![1, 0, 1, 0, 1]
82+
);
83+
}
84+
85+
#[cfg(feature = "example-db")]
86+
#[test]
87+
fn test_canonical_rule_example_spec_builds() {
88+
let example = (canonical_rule_example_specs()
89+
.into_iter()
90+
.find(|spec| spec.id == "minimumvertexcover_to_minimumfeedbackvertexset")
91+
.expect("example spec should be registered")
92+
.build)();
93+
94+
assert_eq!(example.source.problem, "MinimumVertexCover");
95+
assert_eq!(example.target.problem, "MinimumFeedbackVertexSet");
96+
assert_eq!(example.solutions.len(), 1);
97+
assert_eq!(
98+
example.solutions[0].source_config,
99+
example.solutions[0].target_config
100+
);
101+
102+
let source: MinimumVertexCover<SimpleGraph, i32> =
103+
serde_json::from_value(example.source.instance.clone())
104+
.expect("source example deserializes");
105+
let target: MinimumFeedbackVertexSet<i32> =
106+
serde_json::from_value(example.target.instance.clone())
107+
.expect("target example deserializes");
108+
let solution = &example.solutions[0];
109+
110+
let source_metric = source.evaluate(&solution.source_config);
111+
let target_metric = target.evaluate(&solution.target_config);
112+
assert!(
113+
source_metric.is_valid(),
114+
"source witness should be feasible"
115+
);
116+
assert!(
117+
target_metric.is_valid(),
118+
"target witness should be feasible"
119+
);
120+
121+
let best_source = BruteForce::new()
122+
.find_best(&source)
123+
.expect("source example should have an optimum");
124+
let best_target = BruteForce::new()
125+
.find_best(&target)
126+
.expect("target example should have an optimum");
127+
128+
assert_eq!(source_metric, source.evaluate(&best_source));
129+
assert_eq!(target_metric, target.evaluate(&best_target));
130+
}

0 commit comments

Comments
 (0)