Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ecs/src/schedule/graph/tarjan_scc.rs
6849 views
1
use alloc::vec::Vec;
2
use core::{hash::BuildHasher, num::NonZeroUsize};
3
4
use smallvec::SmallVec;
5
6
use crate::schedule::graph::{DiGraph, GraphNodeId};
7
8
/// Create an iterator over *strongly connected components* using Algorithm 3 in
9
/// [A Space-Efficient Algorithm for Finding Strongly Connected Components][1] by David J. Pierce,
10
/// which is a memory-efficient variation of [Tarjan's algorithm][2].
11
///
12
///
13
/// [1]: https://homepages.ecs.vuw.ac.nz/~djp/files/P05.pdf
14
/// [2]: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
15
///
16
/// Returns each strongly strongly connected component (scc).
17
/// The order of node ids within each scc is arbitrary, but the order of
18
/// the sccs is their postorder (reverse topological sort).
19
pub(crate) fn new_tarjan_scc<N: GraphNodeId, S: BuildHasher>(
20
graph: &DiGraph<N, S>,
21
) -> impl Iterator<Item = SmallVec<[N; 4]>> + '_ {
22
// Create a list of all nodes we need to visit.
23
let unchecked_nodes = graph.nodes();
24
25
// For each node we need to visit, we also need to visit its neighbors.
26
// Storing the iterator for each set of neighbors allows this list to be computed without
27
// an additional allocation.
28
let nodes = graph
29
.nodes()
30
.map(|node| NodeData {
31
root_index: None,
32
neighbors: graph.neighbors(node),
33
})
34
.collect::<Vec<_>>();
35
36
TarjanScc {
37
graph,
38
unchecked_nodes,
39
index: 1, // Invariant: index < component_count at all times.
40
component_count: usize::MAX, // Will hold if component_count is initialized to number of nodes - 1 or higher.
41
nodes,
42
stack: Vec::new(),
43
visitation_stack: Vec::new(),
44
start: None,
45
index_adjustment: None,
46
}
47
}
48
49
struct NodeData<Neighbors: Iterator<Item: GraphNodeId>> {
50
root_index: Option<NonZeroUsize>,
51
neighbors: Neighbors,
52
}
53
54
/// A state for computing the *strongly connected components* using [Tarjan's algorithm][1].
55
///
56
/// This is based on [`TarjanScc`] from [`petgraph`].
57
///
58
/// [1]: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
59
/// [`petgraph`]: https://docs.rs/petgraph/0.6.5/petgraph/
60
/// [`TarjanScc`]: https://docs.rs/petgraph/0.6.5/petgraph/algo/struct.TarjanScc.html
61
struct TarjanScc<'graph, N, Hasher, AllNodes, Neighbors>
62
where
63
N: GraphNodeId,
64
Hasher: BuildHasher,
65
AllNodes: Iterator<Item = N>,
66
Neighbors: Iterator<Item = N>,
67
{
68
/// Source of truth [`DiGraph`]
69
graph: &'graph DiGraph<N, Hasher>,
70
/// An [`Iterator`] of [`GraphNodeId`]s from the `graph` which may not have been visited yet.
71
unchecked_nodes: AllNodes,
72
/// The index of the next SCC
73
index: usize,
74
/// A count of potentially remaining SCCs
75
component_count: usize,
76
/// Information about each [`GraphNodeId`], including a possible SCC index and an
77
/// [`Iterator`] of possibly unvisited neighbors.
78
nodes: Vec<NodeData<Neighbors>>,
79
/// A stack of [`GraphNodeId`]s where a SCC will be found starting at the top of the stack.
80
stack: Vec<N>,
81
/// A stack of [`GraphNodeId`]s which need to be visited to determine which SCC they belong to.
82
visitation_stack: Vec<(N, bool)>,
83
/// An index into the `stack` indicating the starting point of a SCC.
84
start: Option<usize>,
85
/// An adjustment to the `index` which will be applied once the current SCC is found.
86
index_adjustment: Option<usize>,
87
}
88
89
impl<
90
'graph,
91
N: GraphNodeId,
92
S: BuildHasher,
93
A: Iterator<Item = N>,
94
Neighbors: Iterator<Item = N>,
95
> TarjanScc<'graph, N, S, A, Neighbors>
96
{
97
/// Compute the next *strongly connected component* using Algorithm 3 in
98
/// [A Space-Efficient Algorithm for Finding Strongly Connected Components][1] by David J. Pierce,
99
/// which is a memory-efficient variation of [Tarjan's algorithm][2].
100
///
101
///
102
/// [1]: https://homepages.ecs.vuw.ac.nz/~djp/files/P05.pdf
103
/// [2]: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
104
///
105
/// Returns `Some` for each strongly strongly connected component (scc).
106
/// The order of node ids within each scc is arbitrary, but the order of
107
/// the sccs is their postorder (reverse topological sort).
108
fn next_scc(&mut self) -> Option<&[N]> {
109
// Cleanup from possible previous iteration
110
if let (Some(start), Some(index_adjustment)) =
111
(self.start.take(), self.index_adjustment.take())
112
{
113
self.stack.truncate(start);
114
self.index -= index_adjustment; // Backtrack index back to where it was before we ever encountered the component.
115
self.component_count -= 1;
116
}
117
118
loop {
119
// If there are items on the visitation stack, then we haven't finished visiting
120
// the node at the bottom of the stack yet.
121
// Must visit all nodes in the stack from top to bottom before visiting the next node.
122
while let Some((v, v_is_local_root)) = self.visitation_stack.pop() {
123
// If this visitation finds a complete SCC, return it immediately.
124
if let Some(start) = self.visit_once(v, v_is_local_root) {
125
return Some(&self.stack[start..]);
126
};
127
}
128
129
// Get the next node to check, otherwise we're done and can return None.
130
let Some(node) = self.unchecked_nodes.next() else {
131
break None;
132
};
133
134
let visited = self.nodes[self.graph.to_index(node)].root_index.is_some();
135
136
// If this node hasn't already been visited (e.g., it was the neighbor of a previously checked node)
137
// add it to the visitation stack.
138
if !visited {
139
self.visitation_stack.push((node, true));
140
}
141
}
142
}
143
144
/// Attempt to find the starting point on the stack for a new SCC without visiting neighbors.
145
/// If a visitation is required, this will return `None` and mark the required neighbor and the
146
/// current node as in need of visitation again.
147
/// If no SCC can be found in the current visitation stack, returns `None`.
148
fn visit_once(&mut self, v: N, mut v_is_local_root: bool) -> Option<usize> {
149
let node_v = &mut self.nodes[self.graph.to_index(v)];
150
151
if node_v.root_index.is_none() {
152
let v_index = self.index;
153
node_v.root_index = NonZeroUsize::new(v_index);
154
self.index += 1;
155
}
156
157
while let Some(w) = self.nodes[self.graph.to_index(v)].neighbors.next() {
158
// If a neighbor hasn't been visited yet...
159
if self.nodes[self.graph.to_index(w)].root_index.is_none() {
160
// Push the current node and the neighbor back onto the visitation stack.
161
// On the next execution of `visit_once`, the neighbor will be visited.
162
self.visitation_stack.push((v, v_is_local_root));
163
self.visitation_stack.push((w, true));
164
165
return None;
166
}
167
168
if self.nodes[self.graph.to_index(w)].root_index
169
< self.nodes[self.graph.to_index(v)].root_index
170
{
171
self.nodes[self.graph.to_index(v)].root_index =
172
self.nodes[self.graph.to_index(w)].root_index;
173
v_is_local_root = false;
174
}
175
}
176
177
if !v_is_local_root {
178
self.stack.push(v); // Stack is filled up when backtracking, unlike in Tarjans original algorithm.
179
return None;
180
}
181
182
// Pop the stack and generate an SCC.
183
let mut index_adjustment = 1;
184
let c = NonZeroUsize::new(self.component_count);
185
let nodes = &mut self.nodes;
186
let start = self
187
.stack
188
.iter()
189
.rposition(|&w| {
190
if nodes[self.graph.to_index(v)].root_index
191
> nodes[self.graph.to_index(w)].root_index
192
{
193
true
194
} else {
195
nodes[self.graph.to_index(w)].root_index = c;
196
index_adjustment += 1;
197
false
198
}
199
})
200
.map(|x| x + 1)
201
.unwrap_or_default();
202
nodes[self.graph.to_index(v)].root_index = c;
203
self.stack.push(v); // Pushing the component root to the back right before getting rid of it is somewhat ugly, but it lets it be included in f.
204
205
self.start = Some(start);
206
self.index_adjustment = Some(index_adjustment);
207
208
Some(start)
209
}
210
}
211
212
impl<
213
'graph,
214
N: GraphNodeId,
215
S: BuildHasher,
216
A: Iterator<Item = N>,
217
Neighbors: Iterator<Item = N>,
218
> Iterator for TarjanScc<'graph, N, S, A, Neighbors>
219
{
220
// It is expected that the `DiGraph` is sparse, and as such wont have many large SCCs.
221
// Returning a `SmallVec` allows this iterator to skip allocation in cases where that
222
// assumption holds.
223
type Item = SmallVec<[N; 4]>;
224
225
fn next(&mut self) -> Option<Self::Item> {
226
let next = SmallVec::from_slice(self.next_scc()?);
227
Some(next)
228
}
229
230
fn size_hint(&self) -> (usize, Option<usize>) {
231
// There can be no more than the number of nodes in a graph worth of SCCs
232
(0, Some(self.nodes.len()))
233
}
234
}
235
236