Abstracting Depth First Search
Depth First Search is applied on a number of algorithms with the same pattern
- Do some work on the node
before
exploring any paths - For edge of the node
- Process the edge
before
performing search on it - Perform search on the edge
- Process the edge
after
all paths from this edge, got explored
- Process the edge
- Do some work on the node
after
all paths from this node, got explored
Different algorithms have different demands on how what the graph state should be and which of those steps are required and in what way
The above can be observed on how the Graph State
realises the DFSearch
trait for Topological sort and Strongly Connected Components implementation
Implementation
As a result, we can define a trait
for any Graph State
structure, that provide the means of how the pre-processing / post-processing steps should be performed and in relation to the required state and behaviour.
It is important to note here the recursive nature of the search and hence the need for self
to maintain the internal state while recursively searching the graph
/// Depth First Search abstraction, enabling a variety of implementations such as, strongly connected components, topological sort, etc
/// The `Path_Search()` default implementation uses the below functions while it leaves their behaviour to the trait implementer
/// - Node pre-processing step fn()
/// - Node post-processing step fn()
/// - Node pre-processing edge fn()
/// - abort recursion fn()
/// - Path return fn()
/// - node state fn()
trait DFSearch {
type Output;
/// work to be done before edges are explored, that is, discovered but not processed
/// uses incl. measuring entry time, set node state, etc
fn pre_process_node(&mut self, node: Node) -> &mut Self;
/// work to be done after the edges have been explored; hence the node is now processed
/// uses incl. measuring exit time, set node parent, save node in path, etc
fn post_process_node(&mut self, node: Node) -> &mut Self;
/// work to be done after the node pre-processed and before the edges is explored
/// uses incl. check for loops, categorize edge into types, etc
/// default implementation does nothing otherwise you have to override
fn pre_process_edge(&mut self, _edge: Edge) -> &mut Self { self }
/// Abort the recursion
/// uses incl. detecting the graph is not Direct acyclic, etc
fn abort(&self) -> bool { false }
/// return the path at position and given the pre/post processing steps
fn path(&self) -> &Self::Output;
/// return whether the node has been seen before
fn is_discovered(&self, node: Node) -> bool;
/// Default implementation of depth first search
fn path_search(&mut self, g: &Graph, start: Node) -> Option<&Self::Output> {
// Entering the node at time tick()
if self.pre_process_node(start).abort() { return None }
// processing the edges
// println!("Enter: {start}:{:?}", self.tracker[start]);
if let Some(edges) = g.edges.get(&start) {
for &dst in edges {
let d = dst;
if self.pre_process_edge(Edge(start,d)).abort() { return None };
if !self.is_discovered(d.into()) {
self.path_search(g, d.into());
}
}
}
// Exiting the node at time tick()
if self.post_process_node(start).abort() { return None };
// println!("Exit: {start}:{:?}", self.tracker[start]);
Some(self.path())
}
}