Skip to main content

weavatrix_graph/traversal_cache/
walk.rs

1use super::TraversalCache;
2use crate::{Direction, NodeIndex, Vec};
3use alloc::collections::VecDeque;
4
5/// Reusable allocation storage for cache-backed BFS and DFS.
6#[derive(Debug, Clone)]
7pub struct TraversalCacheWorkspace {
8    pub(super) marks: Vec<u32>,
9    predecessor: Vec<Option<NodeIndex>>,
10    epoch: u32,
11    queue: VecDeque<NodeIndex>,
12    stack: Vec<NodeIndex>,
13    scratch: Vec<NodeIndex>,
14    pub(super) visited: Vec<NodeIndex>,
15}
16
17impl TraversalCacheWorkspace {
18    #[must_use]
19    pub const fn new() -> Self {
20        Self {
21            marks: Vec::new(),
22            predecessor: Vec::new(),
23            epoch: 0,
24            queue: VecDeque::new(),
25            stack: Vec::new(),
26            scratch: Vec::new(),
27            visited: Vec::new(),
28        }
29    }
30
31    pub(super) fn begin(&mut self, node_count: usize) {
32        if self.marks.len() < node_count {
33            self.marks.resize(node_count, 0);
34        }
35        self.predecessor.resize(node_count, None);
36        self.epoch = self.epoch.wrapping_add(1);
37        if self.epoch == 0 {
38            self.marks.fill(0);
39            self.epoch = 1;
40        }
41        self.queue.clear();
42        self.stack.clear();
43        self.scratch.clear();
44        self.visited.clear();
45    }
46
47    #[inline]
48    pub(super) fn mark(&mut self, node: NodeIndex) -> bool {
49        let mark = &mut self.marks[node.index()];
50        if *mark == self.epoch {
51            return false;
52        }
53        *mark = self.epoch;
54        true
55    }
56}
57
58impl Default for TraversalCacheWorkspace {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// Lazy cache-backed breadth-first traversal.
65pub struct CacheBfs<'cache, 'workspace> {
66    cache: &'cache TraversalCache,
67    workspace: &'workspace mut TraversalCacheWorkspace,
68    direction: Direction,
69}
70
71impl<'cache, 'workspace> CacheBfs<'cache, 'workspace> {
72    fn new(
73        cache: &'cache TraversalCache,
74        start: NodeIndex,
75        direction: Direction,
76        workspace: &'workspace mut TraversalCacheWorkspace,
77    ) -> Self {
78        workspace.begin(cache.node_count());
79        if cache.contains(start) && workspace.mark(start) {
80            workspace.queue.push_back(start);
81        }
82        Self {
83            cache,
84            workspace,
85            direction,
86        }
87    }
88}
89
90impl Iterator for CacheBfs<'_, '_> {
91    type Item = NodeIndex;
92
93    fn next(&mut self) -> Option<Self::Item> {
94        let node = self.workspace.queue.pop_front()?;
95        for_each_neighbor(self.cache, node, self.direction, |neighbor| {
96            if self.workspace.mark(neighbor) {
97                self.workspace.queue.push_back(neighbor);
98            }
99        });
100        Some(node)
101    }
102}
103
104/// Lazy cache-backed depth-first traversal.
105pub struct CacheDfs<'cache, 'workspace> {
106    cache: &'cache TraversalCache,
107    workspace: &'workspace mut TraversalCacheWorkspace,
108    direction: Direction,
109}
110
111impl<'cache, 'workspace> CacheDfs<'cache, 'workspace> {
112    fn new(
113        cache: &'cache TraversalCache,
114        start: NodeIndex,
115        direction: Direction,
116        workspace: &'workspace mut TraversalCacheWorkspace,
117    ) -> Self {
118        workspace.begin(cache.node_count());
119        if cache.contains(start) && workspace.mark(start) {
120            workspace.stack.push(start);
121        }
122        Self {
123            cache,
124            workspace,
125            direction,
126        }
127    }
128}
129
130impl Iterator for CacheDfs<'_, '_> {
131    type Item = NodeIndex;
132
133    fn next(&mut self) -> Option<Self::Item> {
134        let node = self.workspace.stack.pop()?;
135        self.workspace.scratch.clear();
136        for_each_neighbor(self.cache, node, self.direction, |neighbor| {
137            if self.workspace.mark(neighbor) {
138                self.workspace.scratch.push(neighbor);
139            }
140        });
141        while let Some(neighbor) = self.workspace.scratch.pop() {
142            self.workspace.stack.push(neighbor);
143        }
144        Some(node)
145    }
146}
147
148impl TraversalCache {
149    #[must_use]
150    pub fn bfs_iter<'cache, 'workspace>(
151        &'cache self,
152        start: NodeIndex,
153        direction: Direction,
154        workspace: &'workspace mut TraversalCacheWorkspace,
155    ) -> CacheBfs<'cache, 'workspace> {
156        CacheBfs::new(self, start, direction, workspace)
157    }
158
159    #[must_use]
160    pub fn dfs_iter<'cache, 'workspace>(
161        &'cache self,
162        start: NodeIndex,
163        direction: Direction,
164        workspace: &'workspace mut TraversalCacheWorkspace,
165    ) -> CacheDfs<'cache, 'workspace> {
166        CacheDfs::new(self, start, direction, workspace)
167    }
168
169    #[must_use]
170    pub fn bfs(&self, start: NodeIndex, direction: Direction) -> Vec<NodeIndex> {
171        self.bfs_with_workspace(start, direction, &mut TraversalCacheWorkspace::new())
172            .to_vec()
173    }
174
175    #[must_use]
176    pub fn dfs(&self, start: NodeIndex, direction: Direction) -> Vec<NodeIndex> {
177        self.dfs_iter(start, direction, &mut TraversalCacheWorkspace::new())
178            .collect()
179    }
180
181    #[must_use]
182    pub fn reachable(&self, source: NodeIndex, target: NodeIndex, direction: Direction) -> bool {
183        self.contains(target)
184            && self
185                .bfs_iter(source, direction, &mut TraversalCacheWorkspace::new())
186                .any(|node| node == target)
187    }
188
189    #[must_use]
190    pub fn shortest_path(
191        &self,
192        source: NodeIndex,
193        target: NodeIndex,
194        direction: Direction,
195        workspace: &mut TraversalCacheWorkspace,
196    ) -> Option<Vec<NodeIndex>> {
197        if !self.contains(source) || !self.contains(target) {
198            return None;
199        }
200        workspace.begin(self.node_count());
201        workspace.mark(source);
202        workspace.queue.push_back(source);
203        while let Some(node) = workspace.queue.pop_front() {
204            if node == target {
205                return reconstruct(source, target, &workspace.predecessor);
206            }
207            for_each_neighbor(self, node, direction, |neighbor| {
208                if workspace.mark(neighbor) {
209                    workspace.predecessor[neighbor.index()] = Some(node);
210                    workspace.queue.push_back(neighbor);
211                }
212            });
213        }
214        None
215    }
216}
217
218pub(super) fn for_each_neighbor(
219    cache: &TraversalCache,
220    node: NodeIndex,
221    direction: Direction,
222    mut visit: impl FnMut(NodeIndex),
223) {
224    if matches!(direction, Direction::Outgoing | Direction::Both) {
225        cache.for_each_outgoing(node, &mut visit);
226    }
227    if matches!(direction, Direction::Incoming | Direction::Both) {
228        cache.for_each_incoming(node, visit);
229    }
230}
231
232fn reconstruct(
233    source: NodeIndex,
234    target: NodeIndex,
235    predecessor: &[Option<NodeIndex>],
236) -> Option<Vec<NodeIndex>> {
237    let mut path = vec![target];
238    let mut cursor = target;
239    while cursor != source {
240        if path.len() > predecessor.len() {
241            return None;
242        }
243        cursor = predecessor.get(cursor.index()).copied().flatten()?;
244        path.push(cursor);
245    }
246    path.reverse();
247    Some(path)
248}