Skip to main content

problemreductions/models/graph/
minimum_feedback_arc_set.rs

1//! Minimum Feedback Arc Set problem implementation.
2//!
3//! The Feedback Arc Set problem asks for a minimum-weight subset of arcs
4//! whose removal makes a 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: "MinimumFeedbackArcSet",
16        display_name: "Minimum Feedback Arc Set",
17        aliases: &["FAS"],
18        dimensions: &[
19            VariantDimension::new("weight", "i32", &["i32"]),
20        ],
21        module_path: module_path!(),
22        description: "Find minimum weight feedback arc 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: "Arc weights w: A -> R" },
26        ],
27    }
28}
29
30/// The Minimum Feedback Arc Set problem.
31///
32/// Given a directed graph G = (V, A) and weights w_a for each arc,
33/// find a subset A' ⊆ A such that:
34/// - Removing A' from G yields a directed acyclic graph (DAG)
35/// - The total weight Σ_{a ∈ A'} w_a is minimized
36///
37/// # Variables
38///
39/// One binary variable per arc: x_a = 1 means arc a is in the feedback arc set (removed).
40/// The configuration space has dimension m = |A|.
41///
42/// # Example
43///
44/// ```
45/// use problemreductions::models::graph::MinimumFeedbackArcSet;
46/// use problemreductions::topology::DirectedGraph;
47/// use problemreductions::{Problem, Solver, BruteForce};
48///
49/// // Directed cycle: 0->1->2->0
50/// let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]);
51/// let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]);
52///
53/// // Solve with brute force
54/// let solver = BruteForce::new();
55/// let solution = solver.find_witness(&problem).unwrap();
56///
57/// // Minimum FAS has size 1 (remove any single arc to break the cycle)
58/// assert_eq!(solution.iter().sum::<usize>(), 1);
59/// ```
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MinimumFeedbackArcSet<W> {
62    /// The directed graph.
63    graph: DirectedGraph,
64    /// Weights for each arc.
65    weights: Vec<W>,
66}
67
68impl<W: Clone + Default> MinimumFeedbackArcSet<W> {
69    /// Create a Minimum Feedback Arc Set problem from a directed graph with given weights.
70    pub fn new(graph: DirectedGraph, weights: Vec<W>) -> Self {
71        assert_eq!(
72            weights.len(),
73            graph.num_arcs(),
74            "weights length must match graph num_arcs"
75        );
76        Self { graph, weights }
77    }
78
79    /// Get a reference to the underlying directed graph.
80    pub fn graph(&self) -> &DirectedGraph {
81        &self.graph
82    }
83
84    /// Get a reference to the weights slice.
85    pub fn weights(&self) -> &[W] {
86        &self.weights
87    }
88
89    /// Set arc weights.
90    pub fn set_weights(&mut self, weights: Vec<W>) {
91        assert_eq!(
92            weights.len(),
93            self.graph.num_arcs(),
94            "weights length must match graph num_arcs"
95        );
96        self.weights = weights;
97    }
98
99    /// Check if a configuration is a valid feedback arc set.
100    ///
101    /// A configuration is valid if removing the selected arcs makes the graph acyclic.
102    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
103        is_valid_fas(&self.graph, config)
104    }
105}
106
107impl<W: WeightElement> MinimumFeedbackArcSet<W> {
108    /// Check if the problem has non-unit weights.
109    pub fn is_weighted(&self) -> bool {
110        !W::IS_UNIT
111    }
112
113    /// Get the number of vertices in the directed graph.
114    pub fn num_vertices(&self) -> usize {
115        self.graph.num_vertices()
116    }
117
118    /// Get the number of arcs in the directed graph.
119    pub fn num_arcs(&self) -> usize {
120        self.graph.num_arcs()
121    }
122}
123
124impl<W> Problem for MinimumFeedbackArcSet<W>
125where
126    W: WeightElement + crate::variant::VariantParam,
127{
128    const NAME: &'static str = "MinimumFeedbackArcSet";
129    type Value = Min<W::Sum>;
130
131    fn variant() -> Vec<(&'static str, &'static str)> {
132        crate::variant_params![W]
133    }
134
135    fn dims(&self) -> Vec<usize> {
136        vec![2; self.graph.num_arcs()]
137    }
138
139    fn evaluate(&self, config: &[usize]) -> Min<W::Sum> {
140        if !is_valid_fas(&self.graph, config) {
141            return Min(None);
142        }
143        let mut total = W::Sum::zero();
144        for (i, &selected) in config.iter().enumerate() {
145            if selected != 0 {
146                total += self.weights[i].to_sum();
147            }
148        }
149        Min(Some(total))
150    }
151}
152
153/// Check if a configuration forms a valid feedback arc set.
154///
155/// config[i] = 1 means arc i is selected for removal.
156/// The remaining arcs must form a DAG.
157fn is_valid_fas(graph: &DirectedGraph, config: &[usize]) -> bool {
158    let num_arcs = graph.num_arcs();
159    if config.len() != num_arcs {
160        return false;
161    }
162    // kept_arcs[i] = true means arc i is NOT removed (kept in the graph)
163    let kept_arcs: Vec<bool> = config.iter().map(|&x| x == 0).collect();
164    graph.is_acyclic_subgraph(&kept_arcs)
165}
166
167crate::declare_variants! {
168    default MinimumFeedbackArcSet<i32> => "2^num_vertices",
169}
170
171#[cfg(feature = "example-db")]
172pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
173    use crate::topology::DirectedGraph;
174    // 3-node cycle, unit weights; remove one arc to break cycle, cost = 1
175    vec![crate::example_db::specs::ModelExampleSpec {
176        id: "minimum_feedback_arc_set",
177        instance: Box::new(MinimumFeedbackArcSet::new(
178            DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]),
179            vec![1i32, 1, 1],
180        )),
181        optimal_config: vec![0, 0, 1],
182        optimal_value: serde_json::json!(1),
183    }]
184}
185
186#[cfg(test)]
187#[path = "../../unit_tests/models/graph/minimum_feedback_arc_set.rs"]
188mod tests;