Skip to main content

problemreductions/models/graph/
minimum_feedback_vertex_set.rs

1//! Feedback Vertex Set problem implementation.
2//!
3//! The Feedback Vertex Set problem asks for a minimum weight subset of vertices
4//! whose removal makes the directed graph acyclic (a DAG).
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
7use crate::topology::DirectedGraph;
8use crate::traits::Problem;
9use crate::types::{Min, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "MinimumFeedbackVertexSet",
16        display_name: "Minimum Feedback Vertex Set",
17        aliases: &["FVS"],
18        dimensions: &[
19            VariantDimension::new("weight", "i32", &["i32"]),
20        ],
21        module_path: module_path!(),
22        description: "Find minimum weight feedback vertex set in a directed graph",
23        fields: &[
24            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "The directed graph G=(V,A)" },
25            FieldInfo { name: "weights", type_name: "Vec<W>", description: "Vertex weights w: V -> R" },
26        ],
27    }
28}
29
30/// The Minimum Feedback Vertex Set problem.
31///
32/// Given a directed graph G = (V, A) and weights w_v for each vertex,
33/// find a subset F ⊆ V such that:
34/// - Removing F from G yields a directed acyclic graph (DAG)
35/// - The total weight Σ_{v ∈ F} w_v is minimized
36///
37/// # Example
38///
39/// ```
40/// use problemreductions::models::graph::MinimumFeedbackVertexSet;
41/// use problemreductions::topology::DirectedGraph;
42/// use problemreductions::{Problem, Solver, BruteForce};
43///
44/// // Simple 3-cycle: 0 → 1 → 2 → 0
45/// let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]);
46/// let problem = MinimumFeedbackVertexSet::new(graph, vec![1; 3]);
47///
48/// let solver = BruteForce::new();
49/// let solutions = solver.find_all_witnesses(&problem);
50///
51/// // Any single vertex breaks the cycle
52/// assert_eq!(solutions.len(), 3);
53/// ```
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct MinimumFeedbackVertexSet<W> {
56    /// The underlying directed graph.
57    graph: DirectedGraph,
58    /// Weights for each vertex.
59    weights: Vec<W>,
60}
61
62impl<W: Clone + Default> MinimumFeedbackVertexSet<W> {
63    /// Create a Feedback Vertex Set problem from a directed graph with given weights.
64    pub fn new(graph: DirectedGraph, weights: Vec<W>) -> Self {
65        assert_eq!(
66            weights.len(),
67            graph.num_vertices(),
68            "weights length must match graph num_vertices"
69        );
70        Self { graph, weights }
71    }
72
73    /// Get a reference to the underlying directed graph.
74    pub fn graph(&self) -> &DirectedGraph {
75        &self.graph
76    }
77
78    /// Get a reference to the weights slice.
79    pub fn weights(&self) -> &[W] {
80        &self.weights
81    }
82
83    /// Set vertex weights.
84    pub fn set_weights(&mut self, weights: Vec<W>) {
85        assert_eq!(
86            weights.len(),
87            self.graph.num_vertices(),
88            "weights length must match graph num_vertices"
89        );
90        self.weights = weights;
91    }
92
93    /// Check if a configuration is a valid feedback vertex set.
94    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
95        if config.len() != self.graph.num_vertices() {
96            return false;
97        }
98        let keep: Vec<bool> = config.iter().map(|&c| c == 0).collect();
99        self.graph.induced_subgraph(&keep).is_dag()
100    }
101}
102
103impl<W: WeightElement> MinimumFeedbackVertexSet<W> {
104    /// Check if the problem has non-unit weights.
105    pub fn is_weighted(&self) -> bool {
106        !W::IS_UNIT
107    }
108
109    /// Get the number of vertices in the underlying directed graph.
110    pub fn num_vertices(&self) -> usize {
111        self.graph.num_vertices()
112    }
113
114    /// Get the number of arcs in the underlying directed graph.
115    pub fn num_arcs(&self) -> usize {
116        self.graph.num_arcs()
117    }
118}
119
120impl<W> Problem for MinimumFeedbackVertexSet<W>
121where
122    W: WeightElement + crate::variant::VariantParam,
123{
124    const NAME: &'static str = "MinimumFeedbackVertexSet";
125    type Value = Min<W::Sum>;
126
127    fn variant() -> Vec<(&'static str, &'static str)> {
128        crate::variant_params![W]
129    }
130
131    fn dims(&self) -> Vec<usize> {
132        vec![2; self.graph.num_vertices()]
133    }
134
135    fn evaluate(&self, config: &[usize]) -> Min<W::Sum> {
136        if config.len() != self.graph.num_vertices() {
137            return Min(None);
138        }
139        // keep[v] = true if vertex v is NOT selected for removal
140        let keep: Vec<bool> = config.iter().map(|&c| c == 0).collect();
141        let subgraph = self.graph.induced_subgraph(&keep);
142        if !subgraph.is_dag() {
143            return Min(None);
144        }
145        let mut total = W::Sum::zero();
146        for (i, &selected) in config.iter().enumerate() {
147            if selected == 1 {
148                total += self.weights[i].to_sum();
149            }
150        }
151        Min(Some(total))
152    }
153}
154
155crate::declare_variants! {
156    default MinimumFeedbackVertexSet<i32> => "1.9977^num_vertices",
157}
158
159#[cfg(feature = "example-db")]
160pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
161    use crate::topology::DirectedGraph;
162    vec![crate::example_db::specs::ModelExampleSpec {
163        id: "minimum_feedback_vertex_set_i32",
164        instance: Box::new(MinimumFeedbackVertexSet::new(
165            DirectedGraph::new(
166                5,
167                vec![(0, 1), (1, 2), (2, 0), (0, 3), (3, 4), (4, 1), (4, 2)],
168            ),
169            vec![1i32; 5],
170        )),
171        optimal_config: vec![1, 0, 0, 0, 0],
172        optimal_value: serde_json::json!(1),
173    }]
174}
175
176/// Check if a set of vertices is a feedback vertex set (removing them makes the graph a DAG).
177///
178/// # Panics
179/// Panics if `selected.len() != graph.num_vertices()`.
180#[cfg(test)]
181pub(crate) fn is_feedback_vertex_set(graph: &DirectedGraph, selected: &[bool]) -> bool {
182    assert_eq!(
183        selected.len(),
184        graph.num_vertices(),
185        "selected length must match num_vertices"
186    );
187    // keep = NOT selected
188    let keep: Vec<bool> = selected.iter().map(|&s| !s).collect();
189    graph.induced_subgraph(&keep).is_dag()
190}
191
192#[cfg(test)]
193#[path = "../../unit_tests/models/graph/minimum_feedback_vertex_set.rs"]
194mod tests;