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