Skip to main content

weavatrix_graph/algo/
chains.rs

1use super::undirected_snapshot::UndirectedSnapshot;
2use crate::{IndexUndirectedGraphView, Vec};
3
4/// One oriented edge in a DFS chain.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct ChainStep<Node, Edge> {
7    edge: Edge,
8    source: Node,
9    target: Node,
10}
11
12impl<Node: Copy, Edge: Copy> ChainStep<Node, Edge> {
13    /// Returns the original graph edge.
14    #[must_use]
15    pub const fn edge(&self) -> Edge {
16        self.edge
17    }
18
19    /// Returns the step source in chain orientation.
20    #[must_use]
21    pub const fn source(&self) -> Node {
22        self.source
23    }
24
25    /// Returns the step target in chain orientation.
26    #[must_use]
27    pub const fn target(&self) -> Node {
28        self.target
29    }
30}
31
32/// Deterministic edge-disjoint chains of an undirected DFS forest.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ChainDecomposition<Node, Edge> {
35    chains: Vec<Vec<ChainStep<Node, Edge>>>,
36}
37
38impl<Node, Edge> ChainDecomposition<Node, Edge> {
39    /// Returns chains in deterministic DFS discovery order.
40    #[must_use]
41    pub fn chains(&self) -> &[Vec<ChainStep<Node, Edge>>] {
42        &self.chains
43    }
44
45    /// Returns the number of chains.
46    #[must_use]
47    pub fn chain_count(&self) -> usize {
48        self.chains.len()
49    }
50
51    /// Consumes the result and returns its chains.
52    #[must_use]
53    pub fn into_chains(self) -> Vec<Vec<ChainStep<Node, Edge>>> {
54        self.chains
55    }
56}
57
58/// Computes a chain decomposition of every connected component in `O(V + E)`.
59///
60/// Unlike simple-graph-only variants, this preserves parallel edge identities
61/// and represents a self-loop as a one-edge chain.
62#[must_use]
63pub fn chain_decomposition<G>(graph: &G) -> ChainDecomposition<G::Node, G::Edge>
64where
65    G: IndexUndirectedGraphView,
66{
67    chain_decomposition_filtered(graph, |_| true)
68}
69
70/// Computes chains using accepted edges only.
71///
72/// The predicate is evaluated exactly once per edge.
73#[must_use]
74pub fn chain_decomposition_filtered<G, F>(
75    graph: &G,
76    allows_edge: F,
77) -> ChainDecomposition<G::Node, G::Edge>
78where
79    G: IndexUndirectedGraphView,
80    F: Fn(G::Edge) -> bool,
81{
82    let snapshot = UndirectedSnapshot::new(graph, allows_edge);
83    let roots = snapshot.nodes().to_vec();
84    decompose(graph, &snapshot, &roots)
85}
86
87/// Computes chains only in the connected component containing `source`.
88///
89/// Returns `None` when `source` is absent.
90#[must_use]
91pub fn chain_decomposition_from<G>(
92    graph: &G,
93    source: G::Node,
94) -> Option<ChainDecomposition<G::Node, G::Edge>>
95where
96    G: IndexUndirectedGraphView,
97{
98    chain_decomposition_from_filtered(graph, source, |_| true)
99}
100
101/// Computes accepted-edge chains in the component containing `source`.
102///
103/// The predicate is evaluated exactly once per edge. Returns `None` when
104/// `source` is absent.
105#[must_use]
106pub fn chain_decomposition_from_filtered<G, F>(
107    graph: &G,
108    source: G::Node,
109    allows_edge: F,
110) -> Option<ChainDecomposition<G::Node, G::Edge>>
111where
112    G: IndexUndirectedGraphView,
113    F: Fn(G::Edge) -> bool,
114{
115    if !graph.contains_node(source) {
116        return None;
117    }
118    let snapshot = UndirectedSnapshot::new(graph, allows_edge);
119    Some(decompose(graph, &snapshot, &[source]))
120}
121
122fn decompose<G>(
123    graph: &G,
124    snapshot: &UndirectedSnapshot<G>,
125    roots: &[G::Node],
126) -> ChainDecomposition<G::Node, G::Edge>
127where
128    G: IndexUndirectedGraphView,
129{
130    let mut forest = DfsForest::<G>::new(graph);
131    for &root in roots {
132        if forest.discovery[G::node_slot(root)].is_none() {
133            forest.search_from(graph, snapshot, root);
134        }
135    }
136    forest.build_chains()
137}
138
139#[derive(Clone, Copy)]
140struct BackEdge<Node, Edge> {
141    descendant: Node,
142    edge: Edge,
143}
144
145struct Frame<Node, Edge> {
146    node: Node,
147    parent_edge: Option<Edge>,
148    next: usize,
149}
150
151struct DfsForest<G>
152where
153    G: IndexUndirectedGraphView,
154{
155    discovery: Vec<Option<usize>>,
156    parent_node: Vec<Option<G::Node>>,
157    parent_edge: Vec<Option<G::Edge>>,
158    order: Vec<G::Node>,
159    back_edges: Vec<Vec<BackEdge<G::Node, G::Edge>>>,
160    seen_self_loop: Vec<bool>,
161}
162
163impl<G> DfsForest<G>
164where
165    G: IndexUndirectedGraphView,
166{
167    fn new(graph: &G) -> Self {
168        Self {
169            discovery: vec![None; graph.node_bound()],
170            parent_node: vec![None; graph.node_bound()],
171            parent_edge: vec![None; graph.node_bound()],
172            order: Vec::with_capacity(graph.node_count()),
173            back_edges: (0..graph.node_bound()).map(|_| Vec::new()).collect(),
174            seen_self_loop: vec![false; graph.edge_bound()],
175        }
176    }
177
178    fn search_from(&mut self, graph: &G, snapshot: &UndirectedSnapshot<G>, root: G::Node) {
179        self.discover(root, None, None);
180        let mut frames = vec![Frame {
181            node: root,
182            parent_edge: None,
183            next: 0,
184        }];
185        while let Some(frame) = frames.last_mut() {
186            let incident = snapshot.incident(frame.node);
187            if frame.next == incident.len() {
188                frames.pop();
189                continue;
190            }
191            let edge = incident[frame.next];
192            frame.next += 1;
193            if Some(edge) == frame.parent_edge {
194                continue;
195            }
196            let node = frame.node;
197            let Some(neighbor) = graph.opposite(edge, node) else {
198                continue;
199            };
200            if neighbor == node {
201                let edge_slot = G::edge_slot(edge);
202                if !self.seen_self_loop[edge_slot] {
203                    self.seen_self_loop[edge_slot] = true;
204                    self.back_edges[G::node_slot(node)].push(BackEdge {
205                        descendant: node,
206                        edge,
207                    });
208                }
209                continue;
210            }
211            let slot = G::node_slot(node);
212            let neighbor_slot = G::node_slot(neighbor);
213            if self.discovery[neighbor_slot].is_none() {
214                self.discover(neighbor, Some(node), Some(edge));
215                frames.push(Frame {
216                    node: neighbor,
217                    parent_edge: Some(edge),
218                    next: 0,
219                });
220            } else if self.discovery[neighbor_slot] < self.discovery[slot] {
221                self.back_edges[neighbor_slot].push(BackEdge {
222                    descendant: node,
223                    edge,
224                });
225            }
226        }
227    }
228
229    fn discover(&mut self, node: G::Node, parent: Option<G::Node>, edge: Option<G::Edge>) {
230        let slot = G::node_slot(node);
231        self.discovery[slot] = Some(self.order.len());
232        self.parent_node[slot] = parent;
233        self.parent_edge[slot] = edge;
234        self.order.push(node);
235    }
236
237    fn build_chains(self) -> ChainDecomposition<G::Node, G::Edge> {
238        let mut visited = vec![false; self.discovery.len()];
239        let mut chains = Vec::new();
240        for ancestor in self.order {
241            visited[G::node_slot(ancestor)] = true;
242            'back_edges: for back in &self.back_edges[G::node_slot(ancestor)] {
243                let mut chain = vec![ChainStep {
244                    edge: back.edge,
245                    source: ancestor,
246                    target: back.descendant,
247                }];
248                let mut cursor = back.descendant;
249                while !visited[G::node_slot(cursor)] {
250                    visited[G::node_slot(cursor)] = true;
251                    let slot = G::node_slot(cursor);
252                    let Some((parent, edge)) = self.parent_node[slot].zip(self.parent_edge[slot])
253                    else {
254                        continue 'back_edges;
255                    };
256                    chain.push(ChainStep {
257                        edge,
258                        source: cursor,
259                        target: parent,
260                    });
261                    cursor = parent;
262                }
263                chains.push(chain);
264            }
265        }
266        ChainDecomposition { chains }
267    }
268}