Merge branch 'main' into release-0.3.0

This commit is contained in:
2026-07-31 10:16:03 +02:00
12 changed files with 804 additions and 67 deletions
+446 -7
View File
@@ -1,3 +1,33 @@
//! Algorithms for graph topologies.
//!
//! # Dijkstra's algorithm
//!
//! [Dijkstra's algorithm] finds the shortest distances from a source vertex to all other vertices
//! in the graph. Note that for unweighted graphs it has worse time and space complexity than
//! [Breadth-first search](#breadth-first-search).
//!
//! Variants: [`dijkstra`], [`dijkstra_distances`], [`dijkstra_unweighted`],
//! [`dijkstra_distances_unweighted`].
//!
//! # Breadth-first search
//!
//! [Breadth-first search] traverses a graph topology, exploring all neighboring vertices first
//! before descending further into their neighborhoods.
//!
//! Variants: [`bfs`], [`bfs_distances`], [`bfs_find`], [`bfs_find_where`].
//!
//! # Depth-first search
//!
//! [Depth-first search] traverses a graph topology, exploring each branch path as far as possible
//! first before backtracking and exploring other branches.
//!
//! Variants: [`dfs`], [`dfs_visited`], [`dfs_find`], [`dfs_find_where`], [`dfs_find_path`],
//! [`dfs_find_path_where`].
//!
//! [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
//! [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
//! [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
use std::cmp::Ordering;
use std::collections::{BinaryHeap, VecDeque};
@@ -22,12 +52,45 @@ impl<V: Eq> Ord for DistanceOrderedVertex<V> {
}
}
/// Return data type for [`dijkstra`] and [`dijkstra_unweighted`].
pub struct DijkstraResult<V: Copy> {
/// Vertex map of minimum distances from a given `source` vertex.
pub distances: EntityMap<V, Option<u32>>,
/// Vertex map of predecessors on some shortest path from a given `source` vertex.
pub predecessors: EntityMap<V, Option<V>>,
}
// TODO: Generalize the return type of the weight function.
/// [Dijkstra's algorithm] with custom edge weights, returns minimum distances and predecessors.
///
/// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given
/// by `weight` function. Returns the distances from `source` to each vertex, and the predecessors
/// of each vertex on some shortest path from `source` to that vertex. Returns `None` for any vertex
/// not connected to `source`.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dijkstra;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// let e = graph.add_edge(source, target);
/// let mut weights = graph.edge_map(1);
/// weights[e] = 5;
///
/// let result = dijkstra(&graph, source, |e| weights[e]);
/// assert_eq!(result.distances[target], Some(5));
/// assert_eq!(result.predecessors[target], Some(source));
/// ```
///
/// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
pub fn dijkstra<G, W>(graph: &G, source: G::Vertex, weight: W) -> DijkstraResult<G::Vertex>
where
G: GraphTopology,
@@ -43,13 +106,34 @@ where
}
}
pub fn dijkstra_unweighted<G>(graph: &G, source: G::Vertex) -> DijkstraResult<G::Vertex>
where
G: GraphTopology,
{
dijkstra(graph, source, |_| 1)
}
/// [Dijkstra's algorithm] with custom edge weights, returns minimum distances.
///
/// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given
/// by `weight` function. Returns the distances from `source` to each vertex. Returns `None` for any
/// vertex not connected to `source`.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dijkstra_distances;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// let e = graph.add_edge(source, target);
/// let mut weights = graph.edge_map(1);
/// weights[e] = 5;
///
/// let distances = dijkstra_distances(&graph, source, |e| weights[e]);
/// assert_eq!(distances[target], Some(5));
/// ```
///
/// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
pub fn dijkstra_distances<G, W>(
graph: &G,
source: G::Vertex,
@@ -62,6 +146,70 @@ where
dijkstra_impl(graph, source, weight, |_, _| {})
}
/// [Dijkstra's algorithm] with unit edge weights, returns minimum distances and predecessors.
///
/// Calculates the shortest paths from `source` to all vertices in an unweighted `graph`. Returns
/// the distances from `source` to each vertex, and the predecessors of each vertex on some shortest
/// path from `source` to that vertex. Returns `None` for any vertex not connected to `source`.
///
/// Prefer [`bfs`] for unweighted graphs, it has better time and space complexity.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dijkstra_unweighted;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let result = dijkstra_unweighted(&graph, source);
/// assert_eq!(result.distances[target], Some(1));
/// assert_eq!(result.predecessors[target], Some(source));
/// ```
///
/// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
pub fn dijkstra_unweighted<G>(graph: &G, source: G::Vertex) -> DijkstraResult<G::Vertex>
where
G: GraphTopology,
{
dijkstra(graph, source, |_| 1)
}
/// [Dijkstra's algorithm] with unit edge weights, returns minimum distances.
///
/// Calculates the shortest paths from `source` to all vertices in an unweighted `graph`. Returns
/// the distances from `source` to each vertex. Returns `None` for any vertex not connected to
/// `source`.
///
/// Prefer [`bfs_distances`] for unweighted graphs, it has better time and space complexity.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dijkstra_distances_unweighted;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let distances = dijkstra_distances_unweighted(&graph, source);
/// assert_eq!(distances[target], Some(1));
/// ```
///
/// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
pub fn dijkstra_distances_unweighted<G>(
graph: &G,
source: G::Vertex,
@@ -112,11 +260,44 @@ where
distances
}
/// Return data type for [`bfs`].
pub struct BfsResult<V: Copy> {
/// Vertex map of minimum distances from a given `source` vertex.
pub distances: EntityMap<V, Option<u32>>,
/// Vertex map of predecessors on some shortest path from a given `source` vertex.
pub predecessors: EntityMap<V, Option<V>>,
}
/// [Breadth-first search] traversal from `source`, returns distances and predecessors.
///
/// Traverses vertices in `graph` starting from `source`, exploring all neighbors before descending
/// further. Returns the distances from `source` to each vertex, and the predecessors of each vertex
/// on some shortest path from `source` to that vertex. Returns `None` for any vertex not connected
/// to `source`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::bfs;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let result = bfs(&graph, source);
/// assert_eq!(result.distances[target], Some(1));
/// assert_eq!(result.predecessors[target], Some(source));
/// ```
///
/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
pub fn bfs<G>(graph: &G, source: G::Vertex) -> BfsResult<G::Vertex>
where
G: GraphTopology,
@@ -132,6 +313,34 @@ where
}
}
/// [Breadth-first search] traversal from `source`, returns distances.
///
/// Traverses vertices in `graph` starting from `source`, exploring all neighbors before descending
/// further. Returns the distances from `source` to each vertex. Returns `None` for any vertex not
/// connected to `source`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::bfs_distances;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let distances = bfs_distances(&graph, source);
/// assert_eq!(distances[target], Some(1));
/// ```
///
/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
pub fn bfs_distances<G>(graph: &G, source: G::Vertex) -> EntityMap<G::Vertex, Option<u32>>
where
G: GraphTopology,
@@ -139,6 +348,33 @@ where
bfs_impl(graph, source, |_, _| true).distances
}
/// [Breadth-first search] from `source` for `target`, returns the distance.
///
/// Traverses vertices in `graph` starting from `source` until `target` is found, exploring all
/// neighbors before descending further. Returns the minimum distance from `source` to `target` if
/// `target` is a valid vertex of `graph` connected to `source`, or `None` otherwise.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::bfs_find;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// assert_eq!(bfs_find(&graph, source, target), Some(1));
/// ```
///
/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
pub fn bfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<u32>
where
G: GraphTopology,
@@ -146,6 +382,35 @@ where
bfs_find_where(graph, source, |v| v == target).map(|(_, distance)| distance)
}
/// [Breadth-first search] from `source` for a vertex matching `predicate`, returns the vertex and
/// its distance.
///
/// Traverses vertices in `graph` starting from `source` until a vertex satisfying `predicate` is
/// found, exploring all neighbors before descending further. Returns the first vertex satisfying
/// `predicate` and its minimum distance from `source`, or `None` if no such vertex is connected to
/// `source`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::bfs_find_where;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// assert_eq!(bfs_find_where(&graph, source, |v| v == target), Some((target, 1)));
/// ```
///
/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search
pub fn bfs_find_where<G, P>(graph: &G, source: G::Vertex, predicate: P) -> Option<(G::Vertex, u32)>
where
G: GraphTopology,
@@ -194,11 +459,44 @@ where
}
}
/// Return data type for [`dfs`].
pub struct DfsResult<V: Copy> {
/// Vertex map indicating which vertices were visited during the search.
pub visited: EntityMap<V, bool>,
/// Vertex map of predecessors on the DFS tree path from a given `source` vertex.
pub predecessors: EntityMap<V, Option<V>>,
}
/// [Depth-first search] traversal from `source`, returns visited vertices and predecessors.
///
/// Traverses vertices in `graph` starting from `source`, exploring each branch as far as possible
/// before backtracking. Returns for each vertex whether it was visited, and the predecessors of
/// each vertex on the DFS tree path from `source` to that vertex. Any vertex not connected to
/// `source` is marked unvisited and has no predecessor.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let result = dfs(&graph, source);
/// assert!(result.visited[target]);
/// assert_eq!(result.predecessors[target], Some(source));
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs<G>(graph: &G, source: G::Vertex) -> DfsResult<G::Vertex>
where
G: GraphTopology,
@@ -214,6 +512,34 @@ where
}
}
/// [Depth-first search] traversal from `source`, returns visited vertices.
///
/// Traverses vertices in `graph` starting from `source`, exploring each branch as far as possible
/// before backtracking. Returns a map indicating which vertices were visited, i.e. which vertices
/// are connected to `source`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs_visited;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// let visited = dfs_visited(&graph, source);
/// assert!(visited[target]);
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs_visited<G>(graph: &G, source: G::Vertex) -> EntityMap<G::Vertex, bool>
where
G: GraphTopology,
@@ -221,6 +547,33 @@ where
dfs_impl(graph, source, |_, _| true).visited
}
/// [Depth-first search] from `source` for `target`, returns whether it was found.
///
/// Traverses vertices in `graph` starting from `source` until `target` is found, exploring each
/// branch as far as possible before backtracking. Returns `true` if `target` is a valid vertex of
/// `graph` connected to `source`, or `false` otherwise.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs_find;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// assert!(dfs_find(&graph, source, target));
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> bool
where
G: GraphTopology,
@@ -228,6 +581,33 @@ where
dfs_find_where(graph, source, |v| v == target).is_some()
}
/// [Depth-first search] from `source` for a vertex matching `predicate`, returns the vertex.
///
/// Traverses vertices in `graph` starting from `source` until a vertex satisfying `predicate` is
/// found, exploring each branch as far as possible before backtracking. Returns the first vertex
/// satisfying `predicate`, or `None` if no such vertex is connected to `source`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs_find_where;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// graph.add_edge(source, target);
///
/// assert_eq!(dfs_find_where(&graph, source, |v| v == target), Some(target));
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs_find_where<G, P>(graph: &G, source: G::Vertex, predicate: P) -> Option<G::Vertex>
where
G: GraphTopology,
@@ -275,6 +655,35 @@ where
}
}
/// [Depth-first search] from `source` for `target`, returns the path as a sequence of edges.
///
/// Traverses vertices in `graph` starting from `source` until `target` is found, exploring each
/// branch as far as possible before backtracking. Returns some path from `source` to `target` as a
/// sequence of edges in traversal order if `target` is a valid vertex of `graph` connected to
/// `source`, or `None` otherwise. The returned path is empty if and only if `source` equals
/// `target`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs_find_path;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// let e = graph.add_edge(source, target);
///
/// assert_eq!(dfs_find_path(&graph, source, target), Some(vec![e]));
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs_find_path<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<Vec<G::Edge>>
where
G: GraphTopology,
@@ -282,6 +691,36 @@ where
dfs_find_path_where(graph, source, |v| v == target)
}
/// [Depth-first search] from `source` for a vertex matching `predicate`, returns the path as a
/// sequence of edges.
///
/// Traverses vertices in `graph` starting from `source` until a vertex satisfying `predicate` is
/// found, exploring each branch as far as possible before backtracking. Returns some path from
/// `source` to the first vertex satisfying `predicate` as a sequence of edges in traversal order,
/// or `None` if no such vertex is connected to `source`. The returned path is empty if and only if
/// `source` satisfies `predicate`.
///
/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*.
///
/// # Panics
///
/// Panics if `source` is not a valid vertex of `graph`.
///
/// # Examples
///
/// ```
/// # use grapherity::prelude::*;
/// # use grapherity::algorithms::dfs_find_path_where;
/// # use grapherity::models::Graph;
/// let mut graph = Graph::new();
/// let source = graph.add_vertex();
/// let target = graph.add_vertex();
/// let e = graph.add_edge(source, target);
///
/// assert_eq!(dfs_find_path_where(&graph, source, |v| v == target), Some(vec![e]));
/// ```
///
/// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search
pub fn dfs_find_path_where<G, P>(graph: &G, source: G::Vertex, predicate: P) -> Option<Vec<G::Edge>>
where
G: GraphTopology,
+3
View File
@@ -126,12 +126,15 @@
//! [`GraphTopology::Vertex`]: crate::traits::GraphTopology::Vertex
//! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion
#![warn(missing_docs)]
pub mod algorithms;
pub mod generators;
pub mod maps;
pub mod models;
pub mod traits;
/// Convenience re-exports of graph topology traits for common use.
pub mod prelude {
pub use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor};
}
+6 -6
View File
@@ -12,9 +12,9 @@ use std::ops::{Index, IndexMut};
/// which means that the provided index conversion function should map entities to contiguous
/// indices, or indices with relatively few gaps.
///
/// Data allocation happens as copy-on-write, i.e. the backing [`Vec`] is only resized if a value
/// beyond current capacity is written, but the `default` value can transparently be read. No bound
/// or validity checks are performed by the map on the provided entities.
/// Data allocation happens as copy-on-write, i.e. the backing [`Vec`] is only resized if a value is
/// written beyond current capacity, but the `default` value can transparently be read. No bound or
/// validity checks are performed by the map on the provided entity handles.
///
/// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `EntityMap` for
/// vertices or edges, respectively.
@@ -41,12 +41,12 @@ impl<E: Copy, T: Clone> EntityMap<E, T> {
self.data.capacity()
}
/// Extends the internal data storage of the map to `capacity`, pre-filling any new slots with
/// the default value.
/// Expands the internal data storage capacity of the map to `capacity`, Does nothing if
/// capacity is already sufficient.
///
/// Use this before writing data for new graph entities to avoid incremental growth on the first
/// write to each new entity.
pub fn extend(&mut self, capacity: usize) {
pub fn expand(&mut self, capacity: usize) {
if capacity > self.data.len() {
self.data.resize(capacity, self.default.clone());
}
+2
View File
@@ -1,3 +1,5 @@
//! Concrete graph topology models.
pub mod append_graph;
pub mod graph;
+52 -4
View File
@@ -1,9 +1,19 @@
//! [`AppendGraph`], an undirected graph topology supporting addition only.
use crate::maps::EntityMap;
use crate::traits::{GraphTopology, IncidenceCursor};
/// An opaque handle identifying a vertex in an [`AppendGraph`].
///
/// Handles are stable for the lifetime of the graph. Obtain via graph methods like
/// [`AppendGraph::add_vertex`] and [`AppendGraph::vertices`].
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Vertex(usize);
/// An opaque handle identifying an edge in an [`AppendGraph`].
///
/// Handles are stable for the lifetime of the graph. Obtain via graph methods like
/// [`AppendGraph::add_edge`] and [`AppendGraph::edges`].
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Edge(usize);
@@ -19,6 +29,7 @@ impl Edge {
}
}
// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173
struct VertexIncidenceHeader {
incidence_count: usize,
first_incidence: Option<Edge>,
@@ -30,6 +41,9 @@ struct IncidenceEntry {
adjacent: Vertex,
}
/// A resumable cursor over the incidences of a single vertex in an [`AppendGraph`].
///
/// Obtain via [`AppendGraph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance.
#[derive(Copy, Clone)]
pub struct AppendGraphIncidenceCursor {
incidence: Option<Edge>,
@@ -37,16 +51,50 @@ pub struct AppendGraphIncidenceCursor {
impl IncidenceCursor<AppendGraph> for AppendGraphIncidenceCursor {
fn next(&mut self, graph: &AppendGraph) -> Option<(Vertex, Edge)> {
graph.step_incidence(&mut self.incidence)
graph
.step_incidence(&mut self.incidence)
.map(|(v, e)| (v, e.normalize()))
}
}
/// An undirected graph that supports adding vertices and edges, but not deleting them.
///
/// `AppendGraph` is optimised for workloads that incrementally build a graph and query it
/// repeatedly. [`Vertex`] and [`Edge`] handles are never invalidated. Use [`Graph`] instead if you
/// need to remove vertices or edges.
///
/// Incidences are stored as interleaved adjacency lists in a single flat [`Vec`]. In general,
/// vertex neighborhood traversals result in scattered index jumps.
///
/// # Examples
///
/// ```
/// use grapherity::prelude::*;
/// use grapherity::models::AppendGraph;
///
/// let mut graph = AppendGraph::new();
/// let v1 = graph.add_vertex();
/// let v2 = graph.add_vertex();
/// let e = graph.add_edge(v1, v2);
/// assert!(graph.are_adjacent(v1, v2));
/// ```
///
/// # Time and space complexity
///
/// Both [`add_vertex`] and [`add_edge`] run in amortised *O(1)* time. [`degree`] runs in *O(1)*
/// time since vertex degrees are stored. Space complexity is *O(|V| + |E|)*.
///
/// [`add_vertex`]: Self::add_vertex
/// [`add_edge`]: Self::add_edge
/// [`degree`]: Self::degree
/// [`Graph`]: crate::models::Graph
pub struct AppendGraph {
vertices: Vec<VertexIncidenceHeader>,
incidences: Vec<IncidenceEntry>,
}
impl AppendGraph {
/// Creates an empty graph instance with no vertices or edges.
pub fn new() -> Self {
Self {
vertices: vec![],
@@ -54,8 +102,8 @@ impl AppendGraph {
}
}
// Adds a single incidence of an edge, which is composed by two such incidences, to the
// incidences vector.
/// Adds a single incidence of an edge, which is composed by two such incidences, to the
/// incidences vector.
fn add_incidence(&mut self, v1: Vertex, v2: Vertex) {
self.incidences.push(IncidenceEntry {
next: self.vertices[v1.0].first_incidence.take(),
@@ -106,7 +154,7 @@ impl GraphTopology for AppendGraph {
}
fn edge_capacity(&self) -> usize {
self.incidences.len() / 2
self.incidences.capacity() / 2
}
fn edge_map<T: Clone>(&self, default: T) -> EntityMap<Self::Edge, T> {
+85 -11
View File
@@ -1,9 +1,20 @@
//! [`Graph`], an undirected graph topology supporting addition and deletion.
use typed_generational_arena::{Arena, Index};
use crate::maps::EntityMap;
use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor};
/// An opaque handle identifying a vertex in a [`Graph`].
///
/// Handles remain valid until the vertex is explicitly deleted. Obtain via graph methods like
/// [`Graph::add_vertex`] and [`Graph::vertices`].
pub type Vertex = Index<VertexIncidenceHeader, usize, usize>;
/// An opaque handle identifying an edge in a [`Graph`].
///
/// Handles remain valid until the edge is explicitly deleted, or one of its endpoint vertices is
/// deleted. Obtain via graph methods like [`Graph::add_edge`] and [`Graph::edges`].
pub type Edge = Index<IncidenceEntry, usize, usize>;
/// An [`EntityMap`] for [`Graph`] vertices.
@@ -18,11 +29,18 @@ struct VertexSlot(usize);
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct IncidenceSlot(usize);
// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173
/// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not
/// intended for direct external use.
#[doc(hidden)]
pub struct VertexIncidenceHeader {
incidence_count: usize,
first_incidence: Option<IncidenceSlot>,
}
/// `pub` because [`Edge`] references it as a type parameter of the underlying arena. Not intended
/// for direct external use.
#[doc(hidden)]
#[derive(Copy, Clone)]
pub struct IncidenceEntry {
next: Option<IncidenceSlot>,
@@ -39,6 +57,9 @@ impl IncidentEdgeCursor {
}
}
/// A resumable cursor over the incidences of a single vertex in a [`Graph`].
///
/// Obtain via [`Graph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance.
#[derive(Copy, Clone)]
pub struct GraphIncidenceCursor {
incidence: Option<IncidenceSlot>,
@@ -46,12 +67,58 @@ pub struct GraphIncidenceCursor {
impl IncidenceCursor<Graph> for GraphIncidenceCursor {
fn next(&mut self, graph: &Graph) -> Option<(Vertex, Edge)> {
graph
.step_incidence(&mut self.incidence)
.map(|(vs, e)| (graph.vertices.get_idx(vs.0).unwrap(), e))
graph.step_incidence(&mut self.incidence).map(|(vs, e)| {
(
graph.vertices.get_idx(vs.0).unwrap(),
graph.normalize_edge(e),
)
})
}
}
/// An undirected graph that supports adding vertices and edges, and deleting them.
///
/// `Graph` is suited for workloads that need to modify the graph structure over time, i.e. adding
/// amd removing vertices and edges. [`Vertex`] and [`Edge`] handles remain valid until the vertex
/// or edge they identify is explicitly deleted. Use [`AppendGraph`] instead if you only need to
/// append vertices and edges.
///
/// Incidences are stored as interleaved adjacency lists in a generational [`Arena`]. In general,
/// vertex neighborhood traversals result in scattered memory accesses.
///
/// # Examples
///
/// ```
/// use grapherity::prelude::*;
/// use grapherity::models::Graph;
///
/// let mut graph = Graph::new();
/// let v1 = graph.add_vertex();
/// let v2 = graph.add_vertex();
/// let _e = graph.add_edge(v1, v2);
/// assert!(graph.are_adjacent(v1, v2));
/// graph.delete_vertex(v1);
/// assert_eq!(graph.degree(v2), 0);
/// ```
///
/// # Time and space complexity
///
/// Both [`add_vertex`] and [`add_edge`] run in amortised *O(1)* time. [`degree`] runs in *O(1)*
/// time since vertex degrees are stored. [`delete_vertex`] runs in *O(degree(v))* time.
/// [`delete_edge`] runs in *O(degree(u) + degree(v))* time, where *u* and *v* are the edge's
/// endpoints.
///
/// Space complexity is *O(|V| + |E|)*, but freed slots for vertices and edges are not compacted and
/// their allocation is never reclaimed. Capacity only grows. If additions and deletions are both
/// planned, performing deletions first allows freed slots to be reused by subsequent additions,
/// potentially avoiding reallocation.
///
/// [`add_vertex`]: Self::add_vertex
/// [`add_edge`]: Self::add_edge
/// [`degree`]: Self::degree
/// [`delete_vertex`]: Self::delete_vertex
/// [`delete_edge`]: Self::delete_edge
/// [`AppendGraph`]: crate::models::AppendGraph
pub struct Graph {
// TODO: Arena index and generation types could be externalized to Graph.
vertices: Arena<VertexIncidenceHeader, usize, usize>,
@@ -59,6 +126,7 @@ pub struct Graph {
}
impl Graph {
/// Creates an empty graph instance with no vertices or edges.
pub fn new() -> Self {
Self {
vertices: Arena::new(),
@@ -66,8 +134,8 @@ impl Graph {
}
}
// Adds a single incidence of an edge, which is composed by two such incidences, to the
// incidences arena, and returns its index.
/// Adds a single incidence of an edge, which is composed by two such incidences, to the
/// incidences arena, and returns its index.
fn add_incidence(&mut self, v1: Vertex, v2: Vertex) -> Edge {
let edge = self.incidences.insert(IncidenceEntry {
next: self.vertices[v1].first_incidence.take(),
@@ -94,8 +162,9 @@ impl Graph {
(f, e_entry, f_entry)
}
// Updates the source vertex incidence list after the incidence "e" was deleted from the
// incidence arena. "next" is the next incidence after "e" in the source vertex incidence list.
/// Updates the `source` vertex incidence list after the incidence `e` was deleted from the
/// incidence arena. `next` is the next incidence after `e` in the `source` vertex incidence
/// list.
fn update_incidence_list(
&mut self,
e: Edge,
@@ -143,7 +212,12 @@ impl Graph {
}
fn normalize_edge(&self, e: Edge) -> Edge {
self.incidences.get_idx(e.arr_idx() & !1).unwrap()
let i = e.arr_idx();
if i & 1 == 0 {
e
} else {
self.incidences.get_idx(i ^ 1).unwrap()
}
}
}
@@ -270,10 +344,10 @@ impl GraphTopologyDeletion for Graph {
}
}
// The incidence entries are removed before patching the linked lists. This is safe because
// update_incidence_list() searches by raw slot index (IncidenceSlot.0) and only dereferences
// the predecessor, never the removed entries themselves.
fn delete_edge(&mut self, e: Self::Edge) {
// The incidence entries are removed before patching the linked lists. This is safe because
// `update_incidence_list` searches by raw slot index (IncidenceSlot.0) and only
// dereferences the predecessor, never the removed entries themselves.
let (f, e_entry, f_entry) = self.remove_incidence_pair(e);
if e_entry.adjacent != f_entry.adjacent {
self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false);
+2
View File
@@ -1,3 +1,5 @@
//! Test fixture and test macros for graph topology and algorithm implementations.
pub(crate) mod bfs_testing;
pub(crate) mod dfs_testing;
pub(crate) mod dijkstra_testing;
+197 -15
View File
@@ -326,7 +326,7 @@ macro_rules! graph_topology_tests {
assert_eq!(
vertices.iter().filter(|&x| *x == v).count(),
1,
"unexpected vertex {v:?} from the iterator"
"unexpected vertex {v:?} from iterator"
);
}
}
@@ -367,7 +367,7 @@ macro_rules! graph_topology_tests {
.iter()
.position(|w| *w == v)
.expect(&format!(
"unexpected adjacent vertex {v:?} of {:?} from the iterator",
"unexpected adjacent vertex {v:?} of {:?} from iterator",
vertices[4]
));
expected_adjacency.swap_remove(i);
@@ -375,7 +375,7 @@ macro_rules! graph_topology_tests {
assert_eq!(
expected_adjacency.len(),
0,
"expected adjacent vertices {:?} of {:?} were not matched by the iterator",
"expected adjacent vertices {:?} of {:?} were not matched by iterator",
expected_adjacency,
vertices[4]
);
@@ -512,7 +512,7 @@ macro_rules! graph_topology_tests {
assert_eq!(
edges.iter().filter(|&&(f, _, _)| f == e).count(),
1,
"unexpected edge {e:?} from the iterator"
"unexpected edge {e:?} from iterator"
);
}
}
@@ -546,14 +546,14 @@ macro_rules! graph_topology_tests {
.iter()
.position(|f| *f == e)
.expect(&format!(
"unexpected incident edge {e:?} of vertex {:?} from the iterator",
"unexpected incident edge {e:?} of vertex {:?} from iterator",
vertices[i]
));
expected.swap_remove(pos);
}
assert!(
expected.is_empty(),
"expected incident edges {:?} of vertex {:?} were not matched by the iterator",
"expected incident edges {:?} of vertex {:?} were not matched by iterator",
expected,
vertices[i]
);
@@ -600,7 +600,7 @@ macro_rules! graph_topology_tests {
}
assert!(
expected.is_empty(),
"expected incident edges {:?} of vertex {:?} were not matched by the iterator",
"expected incident edges {:?} of vertex {:?} were not matched by iterator",
expected,
vertices[i]
);
@@ -636,14 +636,14 @@ macro_rules! graph_topology_tests {
.iter()
.position(|(v, e)| *v == incidence.0 && *e == incidence.1)
.expect(&format!(
"unexpected incidence {incidence:?} of vertex {:?} from the iterator",
"unexpected incidence {incidence:?} of vertex {:?} from iterator",
vertices[i]
));
expected.swap_remove(pos);
}
assert!(
expected.is_empty(),
"expected incidences {:?} of vertex {:?} were not matched by the iterator",
"expected incidences {:?} of vertex {:?} were not matched by iterator",
expected,
vertices[i]
);
@@ -738,6 +738,131 @@ macro_rules! graph_topology_tests {
}
}
#[test]
fn incidence_cursor_empty() {
use $crate::traits::{GraphTopology, IncidenceCursor};
let mut graph = <$T>::new();
let v = graph.add_vertex();
let mut cursor = graph.incidence_cursor(v);
assert_eq!(
cursor.next(&graph),
None,
"incidence cursor of vertex with degree 0 should immediately be exhausted"
);
}
#[test]
fn incidence_cursor() {
use $crate::traits::{GraphTopology, IncidenceCursor};
let (graph, vertices, _, incidences) = make_test_graph();
for i in 0..10 {
let mut expected = incidences[i].clone();
let mut cursor = graph.incidence_cursor(vertices[i]);
while let Some(incidence) = cursor.next(&graph) {
let pos = expected
.iter()
.position(|(v, e)| *v == incidence.0 && *e == incidence.1)
.expect(&format!(
"unexpected incidence {incidence:?} of vertex {:?} from cursor",
vertices[i]
));
expected.swap_remove(pos);
}
assert!(
expected.is_empty(),
"expected incidences {:?} of vertex {:?} were not matched by cursor",
expected,
vertices[i]
);
}
}
#[test]
fn incidence_cursor_loop_edge() {
use $crate::traits::{GraphTopology, IncidenceCursor};
let mut graph = <$T>::new();
let v = graph.add_vertex();
let e = graph.add_edge(v, v);
let mut cursor = graph.incidence_cursor(v);
assert_eq!(
cursor.next(&graph),
Some((v, e)),
"vertex should be adjacent to itself"
);
assert_eq!(
cursor.next(&graph),
Some((v, e)),
"vertex should be adjacent to itself twice"
);
assert_eq!(
cursor.next(&graph),
None,
"too many incidences from cursor"
);
}
#[test]
fn incidence_cursor_multiple_edges() {
use $crate::traits::{GraphTopology, IncidenceCursor};
let k = 3;
let mut graph = <$T>::new();
let vertices = [graph.add_vertex(), graph.add_vertex()];
let mut edges = Vec::new();
for _ in 0..k {
edges.push(graph.add_edge(vertices[0], vertices[1]));
}
for i in 0..2 {
let mut cursor = graph.incidence_cursor(vertices[i]);
for j in 0..k {
let current = cursor.next(&graph).expect(&format!(
"incidence {j} missing, expected {k} incidences for vertex {:?}",
vertices[i]
));
assert_eq!(
current.0,
vertices[1 - i],
"unexpected adjacent vertex of vertex {:?} in incidence {j}",
vertices[i]
);
assert_eq!(
edges.iter().filter(|e| **e == current.1).count(),
1,
"unexpected incident edge {:?} of vertex {:?}",
current.1,
vertices[i],
);
}
assert_eq!(
cursor.next(&graph),
None,
"too many incidences of {:?} from cursor",
vertices[i]
);
}
}
#[test]
fn incidence_cursor_copy() {
use $crate::traits::{GraphTopology, IncidenceCursor};
// Constructs a graph with two vertices connected to `v`.
let mut graph = <$T>::new();
let v = graph.add_vertex();
for _ in 0..2 {
let u = graph.add_vertex();
graph.add_edge(u, v);
}
let mut c1 = graph.incidence_cursor(v);
assert!(c1.next(&graph).is_some(), "expected first incidence");
// Copies cursor mid-traversal.
let mut c2 = c1;
// Continues iteration with original cursor.
assert!(c1.next(&graph).is_some(), "expected second incidence from original cursor");
assert!(c1.next(&graph).is_none(), "expected original cursor to be exhausted");
// Replays from the copy point with the copied cursor.
assert!(c2.next(&graph).is_some(), "expected second incidence from copied cursor");
assert!(c2.next(&graph).is_none(), "expected copied cursor to be exhausted");
}
#[test]
fn incident_vertices_incidences_consistency() {
use $crate::traits::GraphTopology;
@@ -1131,8 +1256,7 @@ macro_rules! graph_topology_deletion_tests {
for i in 0..10 {
let mut expected: Vec<_> = incidences[i]
.iter()
.filter(|(_, e)| *e != edges[2].0)
.map(|(_, e)| *e)
.filter_map(|(_, e)| (*e != edges[2].0).then_some(*e))
.collect();
assert_eq!(
graph.incident_edges(vertices[i]).count(),
@@ -1165,8 +1289,7 @@ macro_rules! graph_topology_deletion_tests {
for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] {
let mut expected: Vec<_> = incidences[i]
.iter()
.filter(|(v, _)| *v != vertices[2])
.map(|(_, e)| *e)
.filter_map(|(v, e)| (*v != vertices[2]).then_some(*e))
.collect();
assert_eq!(
graph.incident_edges(vertices[i]).count(),
@@ -1241,14 +1364,73 @@ macro_rules! graph_topology_deletion_tests {
.iter()
.position(|(u, e)| *u == incidence.0 && *e == incidence.1)
.expect(&format!(
"unexpected incidence {incidence:?} of vertex {:?} after delete",
"unexpected incidence {incidence:?} of vertex {:?} from iterator after delete",
v
));
expected.swap_remove(pos);
}
assert!(
expected.is_empty(),
"expected incidences {:?} of vertex {:?} not matched after delete",
"expected incidences {:?} of vertex {:?} not matched by iterator after delete",
expected,
v
);
}
#[test]
fn incidence_cursor_after_delete_vertex() {
use $crate::traits::GraphTopologyDeletion;
let (mut graph, vertices, _, incidences) = make_test_graph();
graph.delete_vertex(vertices[2]);
for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] {
let remaining = incidences[i]
.iter()
.filter(|(v, _)| *v != vertices[2])
.cloned()
.collect();
assert_vertex_incidence_cursor(&graph, vertices[i], remaining);
}
}
#[test]
fn incidence_cursor_after_delete_edge() {
use $crate::traits::GraphTopologyDeletion;
let (mut graph, vertices, edges, incidences) = make_test_graph();
// Deletes the edge from vertices[1] to vertices[2].
graph.delete_edge(edges[2].0);
for i in 0..10 {
let remaining = incidences[i]
.iter()
.filter(|(_, e)| *e != edges[2].0)
.cloned()
.collect();
assert_vertex_incidence_cursor(&graph, vertices[i], remaining);
}
}
fn assert_vertex_incidence_cursor(
graph: &$T,
v: <$T as $crate::traits::GraphTopology>::Vertex,
mut expected: Vec<(
<$T as $crate::traits::GraphTopology>::Vertex,
<$T as $crate::traits::GraphTopology>::Edge,
)>,
) {
use $crate::traits::IncidenceCursor;
let mut cursor = graph.incidence_cursor(v);
while let Some(incidence) = cursor.next(graph) {
let pos = expected
.iter()
.position(|(u, e)| *u == incidence.0 && *e == incidence.1)
.expect(&format!(
"unexpected incidence {incidence:?} of vertex {:?} from cursor after delete",
v
));
expected.swap_remove(pos);
}
assert!(
expected.is_empty(),
"expected incidences {:?} of vertex {:?} not matched by cursor after delete",
expected,
v
);
+5 -21
View File
@@ -48,7 +48,7 @@ macro_rules! entity_map_tests {
}
#[test]
fn extend_to_new_vertices() {
fn expand_to_new_vertices() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
graph.add_vertex();
@@ -59,21 +59,21 @@ macro_rules! entity_map_tests {
}
assert!(
map.capacity() < graph.vertex_capacity(),
"precondition: map is stale before extend"
"precondition: map is stale before expand"
);
map.extend(graph.vertex_capacity());
map.expand(graph.vertex_capacity());
assert_eq!(map.capacity(), graph.vertex_capacity());
}
#[test]
fn extend_does_not_overwrite_existing_values() {
fn expand_does_not_overwrite_existing_values() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
let v = graph.add_vertex();
let mut map = graph.vertex_map(0);
map[v] = 5;
graph.add_vertex();
map.extend(graph.vertex_capacity());
map.expand(graph.vertex_capacity());
assert_eq!(map[v], 5);
}
};
@@ -97,22 +97,6 @@ macro_rules! entity_map_deletion_tests {
assert_eq!(map[v1], 1);
}
#[test]
fn capacity_does_not_shrink_after_delete() {
use $crate::traits::GraphTopology;
use $crate::traits::GraphTopologyDeletion;
let mut graph = <$T>::new();
let v1 = graph.add_vertex();
let v2 = graph.add_vertex();
let mut map = graph.vertex_map(0);
map[v1] = 5;
let capacity_before = graph.vertex_capacity();
graph.delete_vertex(v2);
map.extend(graph.vertex_capacity());
assert_eq!(map.capacity(), capacity_before);
assert_eq!(map[v1], 5);
}
#[test]
fn reused_slot_returns_old_value() {
use $crate::traits::GraphTopology;
+4 -1
View File
@@ -1,14 +1,17 @@
//! Core traits for undirected graph topologies.
use crate::maps::EntityMap;
// TODO: Add functions to reserve memory for vertices and edges.
// TODO: Split out GraphTopologyAddition trait.
// TODO: Introduce an Incidence struct.
/// A trait representing an undirected graph topology.
///
/// An undirected graph is a set of vertices and undirected edges, where each edge connects either
/// exactly two vertices or one vertex with itself (loop edge). This trait provides methods for
/// querying a graph topology, iterating over vertices and edges, and adding new vertices and edges.
///
/// # Vertices and Edges
/// # Vertices and edges
///
/// Vertices and edges are identified by opaque handles ([`Vertex`] and [`Edge`]) that implement
/// [`Copy`] and [`Eq`]. Handles remain valid for the lifetime of the graph unless the graph also