blob: b143b8aaae566db444b470c7342c1be019928b2b [file] [log] [blame]
//! Compute the transitive reduction and closure of a directed acyclic graph
//!
//! ## Transitive reduction and closure
//! The *transitive closure* of a graph **G = (V, E)** is the graph **Gc = (V, Ec)**
//! such that **(i, j)** belongs to **Ec** if and only if there is a path connecting
//! **i** to **j** in **G**. The *transitive reduction* of **G** is the graph **Gr
//! = (V, Er)** such that **Er** is minimal wrt. inclusion in **E** and the transitive
//! closure of **Gr** is the same as that of **G**.
//! The transitive reduction is well-defined for acyclic graphs only.
use alloc::{vec, vec::Vec};
use fixedbitset::FixedBitSet;
use petgraph_adjacency_matrix::{AdjacencyList, NodeIndex, UnweightedAdjacencyList};
use petgraph_core::{
deprecated::{
index::{FromIndexType, IndexType, IntoIndexType, SafeCast},
visit::{GraphBase, IntoNeighbors, IntoNeighborsDirected, NodeCompactIndexable, NodeCount},
},
edge::Direction,
};
// TODO: does this need access to the AdjaencyMatrix? Or could we model this with the builder
// methods instead? I don't think so.
/// Creates a representation of the same graph respecting topological order for use in
/// `tred::dag_transitive_reduction_closure`.
///
/// `toposort` must be a topological order on the node indices of `g` (for example obtained
/// from [`toposort`]).
///
/// Returns a pair of a graph `res` and the reciprocal of the topological sort `revmap`.
///
/// `res` is the same graph as `g` with the following differences:
/// * Node and edge weights are stripped,
/// * Node indices are replaced by the corresponding rank in `toposort`,
/// * Iterating on the neighbors of a node respects topological order.
///
/// `revmap` is handy to get back to map indices in `g` to indices in `res`.
/// ```
/// use petgraph_algorithms::dag::dag_to_toposorted_adjacency_list;
/// use petgraph_core::{
/// edge::Directed,
/// id::{DefaultIx, SafeCast},
/// visit::IntoNeighbors,
/// };
/// use petgraph_graph::{Graph, NodeIndex};
///
/// let mut g = Graph::<&str, (), Directed, DefaultIx>::new();
/// let second = g.add_node("second child");
/// let top = g.add_node("top");
/// let first = g.add_node("first child");
/// g.extend_with_edges(&[(top, second), (top, first), (first, second)]);
///
/// let toposort = vec![top, first, second];
///
/// let (res, revmap) = dag_to_toposorted_adjacency_list(&g, &toposort);
///
/// // let's compute the children of top in topological order
/// let children: Vec<NodeIndex> = res
/// .neighbors(revmap[top.index()])
/// .map(|ix| toposort[ix.cast()])
/// .collect();
/// assert_eq!(children, vec![first, second])
/// ```
///
/// Runtime: **O(|V| + |E|)**.
///
/// Space complexity: **O(|V| + |E|)**.
pub fn dag_to_toposorted_adjacency_list<G, Ix: IndexType>(
g: G,
toposort: &[G::NodeId],
) -> (UnweightedAdjacencyList<Ix>, Vec<NodeIndex<Ix>>)
where
G: GraphBase + IntoNeighborsDirected + NodeCompactIndexable + NodeCount,
G::NodeId: IntoIndexType<Index = Ix> + Copy,
{
let mut res = AdjacencyList::with_capacity(g.node_count());
// map from old node index to rank in toposort
let mut revmap = vec![NodeIndex::new(Ix::ZERO); g.node_bound()];
for (ix, &old_ix) in toposort.iter().enumerate() {
let ix: NodeIndex<Ix> = NodeIndex::from_usize(ix);
revmap[old_ix.into_index().cast()] = ix;
let iter = g.neighbors_directed(old_ix, Direction::Incoming);
let new_ix = res.add_node_with_capacity(iter.size_hint().0);
debug_assert_eq!(new_ix, ix);
for old_pre in iter {
let pre = revmap[old_pre.into_index().cast()];
res.add_edge(pre, ix, ());
}
}
(res, revmap)
}
/// Computes the transitive reduction and closure of a DAG.
///
/// The algorithm implemented here comes from [On the calculation of
/// transitive reduction-closure of
/// orders](https://www.sciencedirect.com/science/article/pii/0012365X9390164O) by Habib, Morvan
/// and Rampon.
///
/// The input graph must be in a very specific format: an adjacency
/// list such that:
/// * Node indices are a toposort, and
/// * The neighbors of all nodes are stored in topological order.
/// To get such a representation, use the function [`dag_to_toposorted_adjacency_list`].
///
/// [`dag_to_toposorted_adjacency_list`]: ./fn.dag_to_toposorted_adjacency_list.html
///
/// The output is the pair of the transitive reduction and the transitive closure.
///
/// Runtime complexity: **O(|V| + \sum_{(x, y) \in Er} d(y))** where **d(y)**
/// denotes the outgoing degree of **y** in the transitive closure of **G**.
/// This is still **O(|V|³)** in the worst case like the naive algorithm but
/// should perform better for some classes of graphs.
///
/// Space complexity: **O(|E|)**.
pub fn dag_transitive_reduction_closure<E, Ix: IndexType>(
g: &AdjacencyList<E, Ix>,
) -> (UnweightedAdjacencyList<Ix>, UnweightedAdjacencyList<Ix>) {
let mut tred = AdjacencyList::with_capacity(g.node_count());
let mut tclos = AdjacencyList::with_capacity(g.node_count());
let mut mark = FixedBitSet::with_capacity(g.node_count());
for i in g.node_indices() {
tred.add_node();
tclos.add_node_with_capacity(g.neighbors(i).len());
}
// the algorithm relies on this iterator being toposorted
for i in g.node_indices().rev() {
// the algorighm relies on this iterator being toposorted
for x in g.neighbors(i) {
if !mark[x.cast()] {
tred.add_edge(i, x, ());
tclos.add_edge(i, x, ());
for e in tclos.edge_indices_from(x) {
let y = tclos.edge_endpoints(e).unwrap().1;
if !mark[y.cast()] {
mark.insert(y.cast());
tclos.add_edge(i, y, ());
}
}
}
}
for y in tclos.neighbors(i) {
mark.set(y.cast(), false);
}
}
(tred, tclos)
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use petgraph_adjacency_matrix::{AdjacencyList, NodeIndex};
use petgraph_core::{
edge::Directed,
id::{IndexType, SafeCast},
visit::{
Dfs, EdgeFiltered, EdgeRef, IntoEdgeReferences, IntoNeighbors, IntoNodeIdentifiers,
NodeCount, Visitable,
},
};
use petgraph_graph::Graph;
use petgraph_proptest::dag::graph_dag_strategy;
use proptest::prelude::*;
use crate::dag::{dag_transitive_reduction_closure, toposort};
#[test]
fn simple() {
let mut input = AdjacencyList::new();
let a: NodeIndex<u8> = input.add_node();
let b = input.add_node();
let c = input.add_node();
input.add_edge(a, b, ());
input.add_edge(a, c, ());
input.add_edge(b, c, ());
let (reduction, closure) = dag_transitive_reduction_closure(&input);
assert_eq!(reduction.node_count(), 3);
assert_eq!(closure.node_count(), 3);
assert!(reduction.find_edge(a, b).is_some());
assert!(reduction.find_edge(b, c).is_some());
assert!(reduction.find_edge(a, c).is_none());
assert!(closure.find_edge(a, b).is_some());
assert!(closure.find_edge(b, c).is_some());
assert!(closure.find_edge(a, c).is_some());
}
fn naive_closure_foreach<G, F>(graph: G, mut closure: F)
where
G: Visitable + IntoNodeIdentifiers + IntoNeighbors,
F: FnMut(G::NodeId, G::NodeId),
{
let mut dfs = Dfs::empty(&graph);
for index in graph.node_identifiers() {
dfs.reset(&graph);
dfs.move_to(index);
while let Some(nx) = dfs.next(&graph) {
if index != nx {
closure(index, nx);
}
}
}
}
fn naive_closure<G>(graph: G) -> Vec<(G::NodeId, G::NodeId)>
where
G: Visitable + IntoNodeIdentifiers + IntoNeighbors,
{
let mut result = Vec::new();
naive_closure_foreach(graph, |a, b| result.push((a, b)));
result
}
fn naive_closure_edgecount<G>(graph: G) -> usize
where
G: Visitable + IntoNodeIdentifiers + IntoNeighbors,
{
let mut count = 0;
naive_closure_foreach(graph, |_, _| count += 1);
count
}
#[cfg(not(miri))]
proptest! {
// a bit convoluted, as all functions build on top of each other
#[test]
fn integration(graph in graph_dag_strategy::<Graph<(), (), Directed, u8>>(None, None, None)) {
let toposort = toposort(&graph, None).unwrap();
let (toposorted, reverse_toposort) = super::dag_to_toposorted_adjacency_list(&graph, &toposort);
for (index, node) in toposort.iter().enumerate() {
assert_eq!(index, reverse_toposort[node.index()].cast());
}
let (reduction, closure) = dag_transitive_reduction_closure(&toposorted);
assert_eq!(reduction.node_count(), graph.node_count());
assert_eq!(closure.node_count(), graph.node_count());
// Check the closure
let mut closure_edges: Vec<(_, _)> = closure.edge_references().map(|e| (e.source(), e.target())).collect();
closure_edges.sort();
let mut reduction_closure = naive_closure(&reduction);
reduction_closure.sort();
assert_eq!(closure_edges, reduction_closure);
// Check that the transitive reduction is a reduction
for index in reduction.edge_references() {
let filtered = EdgeFiltered::from_fn(&reduction, |edge| {
edge.source() != index.source() || edge.target() != index.target()
});
let received = naive_closure_edgecount(&filtered);
assert!(received < closure_edges.len());
}
// check that the transitive reduction is included in the original graph
for index in reduction.edge_references() {
let source = toposort[index.source().cast()];
let target = toposort[index.target().cast()];
assert!(graph.find_edge(source, target).is_some());
}
}
}
}