petgraph/algo/
feedback_arc_set.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    ops::{Index, IndexMut},
4};
5
6use crate::{
7    graph::{GraphIndex, NodeIndex},
8    visit::{EdgeRef, GraphProp, IntoEdgeReferences},
9    Directed,
10};
11
12use self::linked_list::{LinkedList, LinkedListEntry};
13
14/// \[Generic\] Finds a [feedback arc set]: a set of edges in the given directed graph, which when
15/// removed, make the graph acyclic.
16///
17/// Uses a [greedy heuristic algorithm] to select a small number of edges, but does not necessarily
18/// find the minimum feedback arc set. Time complexity is roughly **O(|E|)** for an input graph with
19/// edges **E**.
20///
21/// Does not consider edge/node weights when selecting edges for the feedback arc set.
22///
23/// Loops (edges to and from the same node) are always included in the returned set.
24///
25/// # Example
26///
27/// ```
28/// # #[cfg(feature = "stable_graph")] {
29/// use petgraph::{
30///     algo::{greedy_feedback_arc_set, is_cyclic_directed},
31///     graph::EdgeIndex,
32///     stable_graph::StableGraph,
33///     visit::EdgeRef,
34/// };
35///
36/// let mut g: StableGraph<(), ()> = StableGraph::from_edges(&[
37///     (0, 1),
38///     (1, 2),
39///     (2, 3),
40///     (3, 4),
41///     (4, 5),
42///     (5, 0),
43///     (4, 1),
44///     (1, 3),
45/// ]);
46///
47/// assert!(is_cyclic_directed(&g));
48///
49/// let fas: Vec<EdgeIndex> = greedy_feedback_arc_set(&g).map(|e| e.id()).collect();
50///
51/// // Remove edges in feedback arc set from original graph
52/// for edge_id in fas {
53///     g.remove_edge(edge_id);
54/// }
55///
56/// assert!(!is_cyclic_directed(&g));
57/// # }
58/// ```
59///
60/// [feedback arc set]: https://en.wikipedia.org/wiki/Feedback_arc_set
61/// [greedy heuristic algorithm]: https://doi.org/10.1016/0020-0190(93)90079-O
62pub fn greedy_feedback_arc_set<G>(g: G) -> impl Iterator<Item = G::EdgeRef>
63where
64    G: IntoEdgeReferences + GraphProp<EdgeType = Directed>,
65    G::NodeId: GraphIndex,
66    G: crate::visit::NodeCount,
67{
68    let node_seq = good_node_sequence(g.edge_references().map(|e| {
69        (
70            NodeIndex::new(e.source().index()),
71            NodeIndex::new(e.target().index()),
72        )
73    }));
74
75    g.edge_references()
76        .filter(move |e| node_seq[&e.source().index()] >= node_seq[&e.target().index()])
77}
78
79fn good_node_sequence(
80    edge_refs: impl Iterator<Item = (NodeIndex<usize>, NodeIndex<usize>)>,
81) -> HashMap<usize, usize> {
82    let mut nodes = FasNodeContainer { nodes: Vec::new() };
83    let mut buckets = Buckets {
84        sinks_or_isolated: NodeLinkedList::new(),
85        sources: NodeLinkedList::new(),
86        bidirectional_pve_dd: Vec::new(),
87        bidirectional_nve_dd: Vec::new(),
88    };
89    // Lookup of node indices from input graph to indices into `nodes`
90    let mut graph_ix_lookup = HashMap::new();
91
92    // Build node entries
93    for (from_g_ix, to_g_ix) in edge_refs {
94        let mut fas_node_entry = |g_ix: NodeIndex<usize>| -> FasNodeIndex {
95            match graph_ix_lookup.get(&g_ix) {
96                Some(fas_ix) => *fas_ix,
97                None => {
98                    let fas_ix = FasNodeIndex(nodes.nodes.len());
99
100                    nodes.nodes.push(LinkedListEntry::new(FasNode {
101                        graph_ix: g_ix,
102                        out_edges: Vec::new(),
103                        in_edges: Vec::new(),
104                        out_degree: 0,
105                        in_degree: 0,
106                    }));
107
108                    graph_ix_lookup.insert(g_ix, fas_ix);
109
110                    fas_ix
111                }
112            }
113        };
114
115        let from_fas_ix = fas_node_entry(from_g_ix);
116        let to_fas_ix = fas_node_entry(to_g_ix);
117
118        nodes[from_fas_ix].data().out_edges.push(to_fas_ix);
119        nodes[to_fas_ix].data().in_edges.push(from_fas_ix);
120    }
121
122    // Set initial in/out-degrees
123    for entry in nodes.nodes.iter_mut() {
124        let node = entry.data();
125        node.out_degree = node.out_edges.len();
126        node.in_degree = node.in_edges.len();
127    }
128
129    // Add nodes to initial lists
130    for i in 0..nodes.nodes.len() {
131        let fas_ix = FasNodeIndex(i);
132        buckets
133            .suitable_bucket(fas_ix, &mut nodes)
134            .push_front(fas_ix, &mut nodes);
135    }
136
137    let mut s_1 = VecDeque::new();
138    let mut s_2 = VecDeque::new();
139
140    loop {
141        let mut some_moved = false;
142
143        while let Some(sink_fas_ix) = buckets.sinks_or_isolated.pop(&mut nodes) {
144            some_moved = true;
145            buckets.update_neighbour_node_buckets(sink_fas_ix, &mut nodes);
146            s_2.push_front(nodes[sink_fas_ix].data().graph_ix);
147        }
148
149        while let Some(source_fas_ix) = buckets.sources.pop(&mut nodes) {
150            some_moved = true;
151            buckets.update_neighbour_node_buckets(source_fas_ix, &mut nodes);
152            s_1.push_back(nodes[source_fas_ix].data().graph_ix);
153        }
154
155        if let Some(list) = buckets
156            .bidirectional_pve_dd
157            .iter_mut()
158            .rev()
159            .chain(buckets.bidirectional_nve_dd.iter_mut())
160            .find(|b| b.start.is_some())
161        {
162            let highest_dd_fas_ix = list.pop(&mut nodes).unwrap();
163            some_moved = true;
164            buckets.update_neighbour_node_buckets(highest_dd_fas_ix, &mut nodes);
165            s_1.push_back(nodes[highest_dd_fas_ix].data().graph_ix);
166
167            Buckets::trim_bucket_list(&mut buckets.bidirectional_pve_dd);
168            Buckets::trim_bucket_list(&mut buckets.bidirectional_nve_dd);
169        }
170
171        if !some_moved {
172            break;
173        }
174    }
175
176    s_1.into_iter()
177        .chain(s_2)
178        .enumerate()
179        .map(|(seq_order, node_index)| (node_index.index(), seq_order))
180        .collect()
181}
182
183type NodeLinkedList = LinkedList<FasNode, FasNodeContainer, FasNodeIndex>;
184
185#[derive(Debug)]
186struct FasNodeContainer {
187    nodes: Vec<LinkedListEntry<FasNode, FasNodeIndex>>,
188}
189
190impl Index<FasNodeIndex> for FasNodeContainer {
191    type Output = LinkedListEntry<FasNode, FasNodeIndex>;
192
193    fn index(&self, index: FasNodeIndex) -> &Self::Output {
194        &self.nodes[index.0]
195    }
196}
197
198impl IndexMut<FasNodeIndex> for FasNodeContainer {
199    fn index_mut(&mut self, index: FasNodeIndex) -> &mut Self::Output {
200        &mut self.nodes[index.0]
201    }
202}
203
204#[derive(Debug)]
205struct Buckets {
206    sinks_or_isolated: NodeLinkedList,
207    sources: NodeLinkedList,
208    /// Bidirectional nodes with positive-or-0 delta degree
209    bidirectional_pve_dd: Vec<NodeLinkedList>,
210    /// Bidirectional nodes with negative delta degree (index 0 is -1 dd, 1 is -2 etc)
211    bidirectional_nve_dd: Vec<NodeLinkedList>,
212}
213
214#[derive(Clone, Copy, PartialEq, Debug)]
215struct FasNodeIndex(usize);
216
217/// Represents a node from the input graph, tracking its current delta degree
218#[derive(Debug)]
219struct FasNode {
220    /// Node index in input graph.
221    graph_ix: NodeIndex<usize>,
222
223    /// All outward edges from this node (not removed during processing)
224    out_edges: Vec<FasNodeIndex>,
225
226    /// All inward edges from this node (not removed during processing)
227    in_edges: Vec<FasNodeIndex>,
228
229    /// Current out-degree of this node (decremented during processing as connected nodes are
230    /// removed)
231    out_degree: usize,
232
233    /// Current in-degree of this node (decremented during processing as connected nodes are
234    /// removed)
235    in_degree: usize,
236}
237
238impl Buckets {
239    fn suitable_bucket(
240        &mut self,
241        ix: FasNodeIndex,
242        nodes: &mut FasNodeContainer,
243    ) -> &mut NodeLinkedList {
244        let node = nodes[ix].data();
245
246        if node.out_degree == 0 {
247            &mut self.sinks_or_isolated
248        } else if node.in_degree == 0 {
249            &mut self.sources
250        } else {
251            let delta_degree = node.out_degree as isize - node.in_degree as isize;
252
253            if delta_degree >= 0 {
254                let bucket_ix = delta_degree as usize;
255
256                if self.bidirectional_pve_dd.len() <= bucket_ix {
257                    self.bidirectional_pve_dd
258                        .resize_with(bucket_ix + 1, NodeLinkedList::new);
259                }
260
261                &mut self.bidirectional_pve_dd[bucket_ix]
262            } else {
263                let bucket_ix = (-delta_degree - 1) as usize;
264
265                if self.bidirectional_nve_dd.len() <= bucket_ix {
266                    self.bidirectional_nve_dd
267                        .resize_with(bucket_ix + 1, NodeLinkedList::new);
268                }
269
270                &mut self.bidirectional_nve_dd[bucket_ix]
271            }
272        }
273    }
274
275    fn update_neighbour_node_buckets(&mut self, ix: FasNodeIndex, nodes: &mut FasNodeContainer) {
276        for i in 0..nodes[ix].data().out_edges.len() {
277            let out_ix = nodes[ix].data().out_edges[i];
278
279            if out_ix == ix {
280                continue;
281            }
282
283            // Ignore nodes which have already been moved to the good sequence
284            if !nodes[out_ix].is_in_list() {
285                continue;
286            }
287
288            self.suitable_bucket(out_ix, nodes).remove(out_ix, nodes);
289
290            // Other node has lost an in-edge; reduce in-degree by 1
291            nodes[out_ix].data().in_degree -= 1;
292
293            self.suitable_bucket(out_ix, nodes)
294                .push_front(out_ix, nodes);
295        }
296
297        for i in 0..nodes[ix].data().in_edges.len() {
298            let in_ix = nodes[ix].data().in_edges[i];
299
300            if in_ix == ix {
301                continue;
302            }
303
304            // Ignore nodes which have already been moved to the good sequence
305            if !nodes[in_ix].is_in_list() {
306                continue;
307            }
308
309            self.suitable_bucket(in_ix, nodes).remove(in_ix, nodes);
310
311            // Other node has lost an out-edge; reduce out-degree by 1
312            nodes[in_ix].data().out_degree -= 1;
313
314            self.suitable_bucket(in_ix, nodes).push_front(in_ix, nodes);
315        }
316    }
317
318    fn trim_bucket_list(list: &mut Vec<NodeLinkedList>) {
319        let trunc_len = if let Some(highest_populated_index) =
320            (0..list.len()).rev().find(|i| list[*i].start.is_some())
321        {
322            highest_populated_index + 1
323        } else {
324            0
325        };
326
327        list.truncate(trunc_len);
328    }
329}
330
331mod linked_list {
332    use std::{marker::PhantomData, ops::IndexMut};
333
334    #[derive(PartialEq, Debug)]
335    pub struct LinkedList<Data, Container, Ix> {
336        pub start: Option<Ix>,
337        marker: PhantomData<(Data, Container)>,
338    }
339
340    #[derive(Debug)]
341    pub struct LinkedListEntry<Data, Ix> {
342        pos: Option<LinkedListPosition<Ix>>,
343        data: Data,
344    }
345
346    #[derive(Debug)]
347    struct LinkedListPosition<Ix> {
348        prev: Option<Ix>,
349        next: Option<Ix>,
350    }
351
352    impl<Data, Ix> LinkedListEntry<Data, Ix> {
353        pub fn new(data: Data) -> Self {
354            LinkedListEntry { pos: None, data }
355        }
356
357        pub fn data(&mut self) -> &mut Data {
358            &mut self.data
359        }
360
361        pub fn is_in_list(&mut self) -> bool {
362            self.pos.is_some()
363        }
364
365        fn pos_mut(&mut self) -> &mut LinkedListPosition<Ix> {
366            self.pos
367                .as_mut()
368                .expect("expected linked list entry to have populated position")
369        }
370    }
371
372    impl<Data, Container, Ix> LinkedList<Data, Container, Ix>
373    where
374        Container: IndexMut<Ix, Output = LinkedListEntry<Data, Ix>>,
375        Ix: PartialEq + Copy,
376    {
377        pub fn new() -> Self {
378            LinkedList {
379                start: None,
380                marker: PhantomData,
381            }
382        }
383
384        pub fn push_front(&mut self, push_ix: Ix, container: &mut Container) {
385            if let Some(start_ix) = self.start {
386                let entry = &mut container[start_ix];
387                entry.pos_mut().prev = Some(push_ix);
388            }
389
390            let push_entry = &mut container[push_ix];
391            push_entry.pos = Some(LinkedListPosition {
392                next: self.start,
393                prev: None,
394            });
395
396            self.start = Some(push_ix);
397        }
398
399        pub fn pop(&mut self, container: &mut Container) -> Option<Ix> {
400            if let Some(remove_ix) = self.start {
401                self.remove(remove_ix, container);
402                Some(remove_ix)
403            } else {
404                None
405            }
406        }
407
408        /// `remove_ix` **must** be a member of the list headed by `self`
409        pub fn remove(&mut self, remove_ix: Ix, container: &mut Container) {
410            debug_assert!(
411                self.to_vec(container).contains(&remove_ix),
412                "node to remove should be member of current linked list"
413            );
414
415            let remove_entry = &mut container[remove_ix];
416            let ll_entry = remove_entry.pos.take().unwrap();
417
418            if let Some(prev_ix) = ll_entry.prev {
419                let prev_node = &mut container[prev_ix];
420                prev_node.pos_mut().next = ll_entry.next;
421            }
422
423            if let Some(next_ix) = ll_entry.next {
424                let next_node = &mut container[next_ix];
425                next_node.pos_mut().prev = ll_entry.prev;
426            }
427
428            // If the removed node was head of the list
429            if self.start == Some(remove_ix) {
430                self.start = ll_entry.next;
431            }
432        }
433
434        /// For debug purposes
435        fn to_vec(&self, container: &mut Container) -> Vec<Ix> {
436            let mut ixs = Vec::new();
437
438            let mut node_ix = self.start;
439
440            while let Some(n_ix) = node_ix {
441                ixs.push(n_ix);
442
443                node_ix = container[n_ix].pos_mut().next;
444            }
445
446            ixs
447        }
448    }
449}