Add cycle detection for graphs
This commit is contained in:
parent
9ad1e7c46c
commit
4fd9b9944f
@ -1,5 +1,5 @@
|
||||
use super::super::indexed_vec::IndexVec;
|
||||
use super::{DirectedGraph, WithNumNodes, WithSuccessors};
|
||||
use super::{DirectedGraph, WithNumNodes, WithSuccessors, WithStartNode};
|
||||
use crate::bit_set::BitSet;
|
||||
|
||||
#[cfg(test)]
|
||||
@ -85,3 +85,205 @@ where
|
||||
Some(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows searches to terminate early with a value.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ControlFlow<T> {
|
||||
Break(T),
|
||||
Continue,
|
||||
}
|
||||
|
||||
/// The status of a node in the depth-first search.
|
||||
///
|
||||
/// See the documentation of `TriColorDepthFirstSearch` to see how a node's status is updated
|
||||
/// during DFS.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum NodeStatus {
|
||||
/// This node has been examined by the depth-first search but is not yet `Settled`.
|
||||
///
|
||||
/// Also referred to as "gray" or "discovered" nodes in [CLR][].
|
||||
///
|
||||
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
|
||||
Visited,
|
||||
|
||||
/// This node and all nodes reachable from it have been examined by the depth-first search.
|
||||
///
|
||||
/// Also referred to as "black" or "finished" nodes in [CLR][].
|
||||
///
|
||||
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
|
||||
Settled,
|
||||
}
|
||||
|
||||
struct Event<N> {
|
||||
node: N,
|
||||
becomes: NodeStatus,
|
||||
}
|
||||
|
||||
/// A depth-first search that also tracks when all successors of a node have been examined.
|
||||
///
|
||||
/// This is based on the DFS described in [Introduction to Algorithms (1st ed.)][CLR], hereby
|
||||
/// referred to as **CLR**. However, we use the terminology in [`NodeStatus`][] above instead of
|
||||
/// "discovered"/"finished" or "white"/"grey"/"black". Each node begins the search with no status,
|
||||
/// becomes `Visited` when it is first examined by the DFS and is `Settled` when all nodes
|
||||
/// reachable from it have been examined. This allows us to differentiate between "tree", "back"
|
||||
/// and "forward" edges (see [`TriColorVisitor::node_examined`]).
|
||||
///
|
||||
/// Unlike the pseudocode in [CLR][], this implementation is iterative and does not use timestamps.
|
||||
/// We accomplish this by storing `Event`s on the stack that result in a (possible) state change
|
||||
/// for each node. A `Visited` event signifies that we should examine this node if it has not yet
|
||||
/// been `Visited` or `Settled`. When a node is examined for the first time, we mark it as
|
||||
/// `Visited` and push a `Settled` event for it on stack followed by `Visited` events for all of
|
||||
/// its predecessors, scheduling them for examination. Multiple `Visited` events for a single node
|
||||
/// may exist on the stack simultaneously if a node has multiple predecessors, but only one
|
||||
/// `Settled` event will ever be created for each node. After all `Visited` events for a node's
|
||||
/// successors have been popped off the stack (as well as any new events triggered by visiting
|
||||
/// those successors), we will pop off that node's `Settled` event.
|
||||
///
|
||||
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
|
||||
/// [`NodeStatus`]: ./enum.NodeStatus.html
|
||||
/// [`TriColorVisitor::node_examined`]: ./trait.TriColorVisitor.html#method.node_examined
|
||||
pub struct TriColorDepthFirstSearch<'graph, G>
|
||||
where
|
||||
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
|
||||
{
|
||||
graph: &'graph G,
|
||||
stack: Vec<Event<G::Node>>,
|
||||
visited: BitSet<G::Node>,
|
||||
settled: BitSet<G::Node>,
|
||||
}
|
||||
|
||||
impl<G> TriColorDepthFirstSearch<'graph, G>
|
||||
where
|
||||
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
|
||||
{
|
||||
pub fn new(graph: &'graph G) -> Self {
|
||||
TriColorDepthFirstSearch {
|
||||
graph,
|
||||
stack: vec![],
|
||||
visited: BitSet::new_empty(graph.num_nodes()),
|
||||
settled: BitSet::new_empty(graph.num_nodes()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a depth-first search, starting from the given `root`.
|
||||
///
|
||||
/// This won't visit nodes that are not reachable from `root`.
|
||||
pub fn run_from<V>(mut self, root: G::Node, visitor: &mut V) -> Option<V::BreakVal>
|
||||
where
|
||||
V: TriColorVisitor<G>,
|
||||
{
|
||||
use NodeStatus::{Visited, Settled};
|
||||
|
||||
self.stack.push(Event { node: root, becomes: Visited });
|
||||
|
||||
loop {
|
||||
match self.stack.pop()? {
|
||||
Event { node, becomes: Settled } => {
|
||||
let not_previously_settled = self.settled.insert(node);
|
||||
assert!(not_previously_settled, "A node should be settled exactly once");
|
||||
if let ControlFlow::Break(val) = visitor.node_settled(node) {
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
|
||||
Event { node, becomes: Visited } => {
|
||||
let not_previously_visited = self.visited.insert(node);
|
||||
let prior_status = if not_previously_visited {
|
||||
None
|
||||
} else if self.settled.contains(node) {
|
||||
Some(Settled)
|
||||
} else {
|
||||
Some(Visited)
|
||||
};
|
||||
|
||||
if let ControlFlow::Break(val) = visitor.node_examined(node, prior_status) {
|
||||
return Some(val);
|
||||
}
|
||||
|
||||
// If this node has already been examined, we are done.
|
||||
if prior_status.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, push a `Settled` event for this node onto the stack, then
|
||||
// schedule its successors for examination.
|
||||
self.stack.push(Event { node, becomes: Settled });
|
||||
for succ in self.graph.successors(node) {
|
||||
self.stack.push(Event { node: succ, becomes: Visited });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<G> TriColorDepthFirstSearch<'graph, G>
|
||||
where
|
||||
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors + WithStartNode,
|
||||
{
|
||||
/// Performs a depth-first search, starting from `G::start_node()`.
|
||||
///
|
||||
/// This won't visit nodes that are not reachable from the start node.
|
||||
pub fn run_from_start<V>(self, visitor: &mut V) -> Option<V::BreakVal>
|
||||
where
|
||||
V: TriColorVisitor<G>,
|
||||
{
|
||||
let root = self.graph.start_node();
|
||||
self.run_from(root, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// What to do when a node is examined or becomes `Settled` during DFS.
|
||||
pub trait TriColorVisitor<G>
|
||||
where
|
||||
G: ?Sized + DirectedGraph,
|
||||
{
|
||||
/// The value returned by this search.
|
||||
type BreakVal;
|
||||
|
||||
/// Called when a node is examined by the depth-first search.
|
||||
///
|
||||
/// By checking the value of `prior_status`, this visitor can determine whether the edge
|
||||
/// leading to this node was a tree edge (`None`), forward edge (`Some(Settled)`) or back edge
|
||||
/// (`Some(Visited)`). For a full explanation of each edge type, see the "Depth-first Search"
|
||||
/// chapter in [CLR][] or [wikipedia][].
|
||||
///
|
||||
/// If you want to know *both* nodes linked by each edge, you'll need to modify
|
||||
/// `TriColorDepthFirstSearch` to store a `source` node for each `Visited` event.
|
||||
///
|
||||
/// [wikipedia]: https://en.wikipedia.org/wiki/Depth-first_search#Output_of_a_depth-first_search
|
||||
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
|
||||
fn node_examined(
|
||||
&mut self,
|
||||
_target: G::Node,
|
||||
_prior_status: Option<NodeStatus>,
|
||||
) -> ControlFlow<Self::BreakVal> {
|
||||
ControlFlow::Continue
|
||||
}
|
||||
|
||||
/// Called after all nodes reachable from this one have been examined.
|
||||
fn node_settled(&mut self, _target: G::Node) -> ControlFlow<Self::BreakVal> {
|
||||
ControlFlow::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// This `TriColorVisitor` looks for back edges in a graph, which indicate that a cycle exists.
|
||||
pub struct CycleDetector;
|
||||
|
||||
impl<G> TriColorVisitor<G> for CycleDetector
|
||||
where
|
||||
G: ?Sized + DirectedGraph,
|
||||
{
|
||||
type BreakVal = ();
|
||||
|
||||
fn node_examined(
|
||||
&mut self,
|
||||
_node: G::Node,
|
||||
prior_status: Option<NodeStatus>,
|
||||
) -> ControlFlow<Self::BreakVal> {
|
||||
match prior_status {
|
||||
Some(NodeStatus::Visited) => ControlFlow::Break(()),
|
||||
_ => ControlFlow::Continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,3 +9,14 @@ fn diamond_post_order() {
|
||||
let result = post_order_from(&graph, 0);
|
||||
assert_eq!(result, vec![3, 1, 2, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_cyclic() {
|
||||
use super::super::is_cyclic;
|
||||
|
||||
let diamond_acyclic = TestGraph::new(0, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
|
||||
let diamond_cyclic = TestGraph::new(0, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
|
||||
|
||||
assert!(!is_cyclic(&diamond_acyclic));
|
||||
assert!(is_cyclic(&diamond_cyclic));
|
||||
}
|
||||
|
@ -81,3 +81,13 @@ where
|
||||
+ WithNumNodes,
|
||||
{
|
||||
}
|
||||
|
||||
/// Returns `true` if the graph has a cycle that is reachable from the start node.
|
||||
pub fn is_cyclic<G>(graph: &G) -> bool
|
||||
where
|
||||
G: ?Sized + DirectedGraph + WithStartNode + WithSuccessors + WithNumNodes,
|
||||
{
|
||||
iterate::TriColorDepthFirstSearch::new(graph)
|
||||
.run_from_start(&mut iterate::CycleDetector)
|
||||
.is_some()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user