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::{CreateSpec, 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", "i64", &["i64"]),
20        ],
21        category: crate::registry::ProblemCategory::Graph,
22        module_path: module_path!(),
23        description: "Find minimum weight feedback arc set in a directed graph",
24        fields: MinimumFeedbackArcSetCreateSpec::FIELDS,
25    }
26}
27
28/// The Minimum Feedback Arc Set problem.
29///
30/// Given a directed graph G = (V, A) and weights w_a for each arc,
31/// find a subset A' ⊆ A such that:
32/// - Removing A' from G yields a directed acyclic graph (DAG)
33/// - The total weight Σ_{a ∈ A'} w_a is minimized
34///
35/// # Variables
36///
37/// One binary variable per arc: x_a = 1 means arc a is in the feedback arc set (removed).
38/// The configuration space has dimension m = |A|.
39///
40/// # Example
41///
42/// ```
43/// use problemreductions::models::graph::MinimumFeedbackArcSet;
44/// use problemreductions::topology::DirectedGraph;
45/// use problemreductions::{Problem, BruteForce};
46///
47/// // Directed cycle: 0->1->2->0
48/// let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]);
49/// let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]);
50///
51/// // Solve with brute force
52/// let solver = BruteForce::new();
53/// let solution = solver.solve(&problem).unwrap().unwrap();
54///
55/// // Minimum FAS has size 1 (remove any single arc to break the cycle)
56/// assert_eq!(solution.iter().filter(|&&selected| selected).count(), 1);
57/// ```
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct MinimumFeedbackArcSet<W> {
60    /// The directed graph.
61    graph: DirectedGraph,
62    /// Weights for each arc.
63    weights: Vec<W>,
64}
65
66#[derive(Debug, Deserialize, crate::CreateSpec)]
67struct MinimumFeedbackArcSetCreateSpec {
68    /// The directed graph.
69    graph: DirectedGraph,
70    /// Arc weights; defaults to one per arc.
71    weights: Option<Vec<i64>>,
72}
73impl TryFrom<MinimumFeedbackArcSetCreateSpec> for MinimumFeedbackArcSet<i64> {
74    type Error = crate::registry::ConstructionError;
75    fn try_from(spec: MinimumFeedbackArcSetCreateSpec) -> Result<Self, Self::Error> {
76        let count = spec.graph.num_arcs();
77        let weights = spec.weights.unwrap_or_else(|| vec![1; count]);
78        if weights.len() != count {
79            return Err(format!("weights has {} entries, expected {count}", weights.len()).into());
80        }
81        Ok(Self::new(spec.graph, weights))
82    }
83}
84
85impl<W: Clone + Default> MinimumFeedbackArcSet<W> {
86    /// Create a Minimum Feedback Arc Set problem from a directed graph with given weights.
87    pub fn new(graph: DirectedGraph, weights: Vec<W>) -> Self {
88        assert_eq!(
89            weights.len(),
90            graph.num_arcs(),
91            "weights length must match graph num_arcs"
92        );
93        Self { graph, weights }
94    }
95
96    /// Get a reference to the underlying directed graph.
97    pub fn graph(&self) -> &DirectedGraph {
98        &self.graph
99    }
100
101    /// Get a reference to the weights slice.
102    pub fn weights(&self) -> &[W] {
103        &self.weights
104    }
105
106    /// Set arc weights.
107    pub fn set_weights(&mut self, weights: Vec<W>) {
108        assert_eq!(
109            weights.len(),
110            self.graph.num_arcs(),
111            "weights length must match graph num_arcs"
112        );
113        self.weights = weights;
114    }
115
116    /// Check if a configuration is a valid feedback arc set.
117    ///
118    /// A configuration is valid if removing the selected arcs makes the graph acyclic.
119    pub fn is_valid_solution(&self, config: &[bool]) -> bool {
120        is_valid_fas(&self.graph, config)
121    }
122}
123
124impl<W: WeightElement> MinimumFeedbackArcSet<W> {
125    /// Check if the problem has non-unit weights.
126    pub fn is_weighted(&self) -> bool {
127        !W::IS_UNIT
128    }
129
130    /// Get the number of vertices in the directed graph.
131    pub fn num_vertices(&self) -> usize {
132        self.graph.num_vertices()
133    }
134
135    /// Get the number of arcs in the directed graph.
136    pub fn num_arcs(&self) -> usize {
137        self.graph.num_arcs()
138    }
139}
140
141impl<W> Problem for MinimumFeedbackArcSet<W>
142where
143    W: WeightElement + crate::variant::VariantParam,
144{
145    const NAME: &'static str = "MinimumFeedbackArcSet";
146    type Solution = Vec<bool>;
147    type Value = Min<W::Sum>;
148
149    crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
150
151    fn variant() -> Vec<(&'static str, &'static str)> {
152        crate::variant_params![W]
153    }
154
155    fn evaluate(
156        &self,
157        config: &Self::Solution,
158    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
159        if config.len() != self.graph.num_arcs() {
160            return Err(crate::traits::EvaluationError::InvalidConfiguration(
161                "arc-selection length does not match the graph".into(),
162            ));
163        }
164        Ok({
165            if !is_valid_fas(&self.graph, config) {
166                return Ok(Min(None));
167            }
168            let mut total = W::Sum::zero();
169            for (i, &selected) in config.iter().enumerate() {
170                if selected {
171                    total = W::checked_add_to_sum(
172                        total,
173                        self.weights[i].to_sum(),
174                        "summing selected feedback-arc weights",
175                    )?;
176                }
177            }
178            Min(Some(total))
179        })
180    }
181}
182
183impl<W> crate::solvers::BruteForceProblem for MinimumFeedbackArcSet<W>
184where
185    W: WeightElement + crate::variant::VariantParam,
186{
187    fn dimensions(&self) -> Vec<usize> {
188        vec![2; self.graph.num_arcs()]
189    }
190}
191
192/// Check if a configuration forms a valid feedback arc set.
193///
194/// config[i] = 1 means arc i is selected for removal.
195/// The remaining arcs must form a DAG.
196fn is_valid_fas(graph: &DirectedGraph, config: &[bool]) -> bool {
197    let num_arcs = graph.num_arcs();
198    if config.len() != num_arcs {
199        return false;
200    }
201    // kept_arcs[i] = true means arc i is NOT removed (kept in the graph)
202    let kept_arcs: Vec<bool> = config.iter().map(|&removed| !removed).collect();
203    graph.is_acyclic_subgraph(&kept_arcs)
204}
205
206crate::declare_variants! {
207    default MinimumFeedbackArcSet<i64> => "2^num_vertices" create MinimumFeedbackArcSetCreateSpec,
208}
209
210crate::register_brute_force! {
211    MinimumFeedbackArcSet<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
212}
213
214#[cfg(feature = "example-db")]
215pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
216    use crate::topology::DirectedGraph;
217    // 3-node cycle, unit weights; remove one arc to break cycle, cost = 1
218    vec![crate::example_db::specs::ModelExampleSpec {
219        id: "minimum_feedback_arc_set",
220        instance: Box::new(MinimumFeedbackArcSet::new(
221            DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]),
222            vec![1i64, 1, 1],
223        )),
224        optimal_config: serde_json::json!(vec![false, false, true]),
225        optimal_value: serde_json::json!(1),
226    }]
227}
228
229#[cfg(test)]
230#[path = "../../unit_tests/models/graph/minimum_feedback_arc_set.rs"]
231mod tests;