From a89086253a0f858c2cbbe50e0d87bd5e9763cba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 17 Aug 2026 10:25:52 +0200 Subject: [PATCH] Add FrozenGraph (without tests) --- README.md | 2 +- src/lib.rs | 17 ++- src/models.rs | 6 +- src/models/append_graph.rs | 4 +- src/models/frozen_graph.rs | 250 +++++++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 src/models/frozen_graph.rs diff --git a/README.md b/README.md index 417af7c..b471e97 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This crate provides data structures to model graphs and to perform algorithms on Currently supported are: -* Undirected graph types `Graph` and `AppendGraph`, built on a flat, index-based adjacency list, +* Undirected graph types `Graph`, `AppendGraph`, and `FrozenGraph`, built on a flat, index-based adjacency list, * Maps to freely associate custom data with vertices and edges, * Connectivity and pathing algorithms: Dijkstra's algorithm, DFS, and BFS in different variants, * Exposed traits to implement custom graph data structures or algorithms. diff --git a/src/lib.rs b/src/lib.rs index 3b617ef..014e586 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,8 @@ //! //! Currently supported are: //! -//! * Undirected graph types [`Graph`] and [`AppendGraph`] built on a flat, index-based adjacency -//! list, +//! * Undirected graph types [`Graph`], [`AppendGraph`], and [`FrozenGraph`] built on a flat, +//! index-based adjacency list, //! * [`ElementMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different //! variants, @@ -62,9 +62,11 @@ //! //! # Graph types //! -//! [`Graph`] and [`AppendGraph`] both implement [`GraphTopology`] using a flat, index-based adjacency -//! list to store edges, which means that vertex and edge insertions happen in `O(1)`. Degree -//! lookups are also `O(1)`. +//! [`Graph`], [`AppendGraph`], and [`FrozenGraph`] all implement [`GraphTopology`] using a flat, +//! index-based adjacency list to store edges. Degree lookups run in `O(1)`. +//! +//! [`Graph`] and [`AppendGraph`] implement [`GraphTopologyAddition`], which provides functionality +//! for iterative graph construction. Vertex and edge insertions happen in `O(1)`. //! //! [`Graph`] additionally implements [`GraphTopologyDeletion`], thereby supporting deletion of //! vertices and edges. @@ -73,6 +75,10 @@ //! be smaller and more performant compared to [`Graph`] by not requiring the per-element generation //! tracking needed for stable handles after deletion. //! +//! [`FrozenGraph`] uses a memory layout that is more cache-friendly for traversals than +//! [`AppendGraph`], but the trade-off is that its topology must be constructed during its +//! initialization. It cannot be modified afterward. +//! //! # Vertices and edges //! //! Graphs in this library use stable indices [`GraphTopology::Vertex`] and [`GraphTopology::Edge`] @@ -119,6 +125,7 @@ //! [DFS]: crate::algorithms::dfs //! [`ElementMap`]: crate::maps::ElementMap //! [`AppendGraph`]: crate::models::AppendGraph +//! [`FrozenGraph`]: crate::models::FrozenGraph //! [`Graph`]: crate::models::Graph //! [`traits`]: crate::traits //! [`GraphTopology`]: crate::traits::GraphTopology diff --git a/src/models.rs b/src/models.rs index 1f302c8..0834a4a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,13 @@ //! Concrete graph topology models. pub mod append_graph; +pub mod frozen_graph; pub mod graph; -// TODO: Compressed-sparse-row graph model. - pub use append_graph::{ AppendGraph, AppendGraphEdgeMap, AppendGraphIncidence, AppendGraphVertexMap, }; +pub use frozen_graph::{ + FrozenGraph, FrozenGraphEdgeMap, FrozenGraphIncidence, FrozenGraphVertexMap, +}; pub use graph::{Graph, GraphEdgeMap, GraphIncidence, GraphVertexMap}; diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index b06971d..ad7de88 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -67,7 +67,8 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// /// `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. +/// need to remove vertices or edges. Use [`FrozenGraph`] if you do not need incremental +/// construction. /// /// Incidences are stored as interleaved adjacency lists in a single flat [`Vec`]. In general, /// vertex neighborhood traversals result in scattered index jumps. @@ -93,6 +94,7 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// [`add_vertex`]: Self::add_vertex /// [`add_edge`]: Self::add_edge /// [`degree`]: Self::degree +/// [`FrozenGraph`]: crate::models::FrozenGraph /// [`Graph`]: crate::models::Graph pub struct AppendGraph { vertices: Vec, diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs new file mode 100644 index 0000000..b2980dc --- /dev/null +++ b/src/models/frozen_graph.rs @@ -0,0 +1,250 @@ +//! [`FrozenGraph`], an undirected graph topology supporting only one-time construction. + +use std::ops::IndexMut; + +use crate::maps::ElementMap; +use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; + +/// An opaque handle identifying a vertex in a [`FrozenGraph`]. +/// +/// Handles are stable for the lifetime of the graph. Obtain via graph methods like +/// [`FrozenGraph::vertices`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct Vertex(usize); + +/// An opaque handle identifying an edge in a [`FrozenGraph`]. +/// +/// Handles are stable for the lifetime of the graph. Obtain via graph methods like +/// [`FrozenGraph::edges`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct Edge(usize); + +/// An [`ElementMap`] for [`FrozenGraph`] vertices. +pub type FrozenGraphVertexMap = ElementMap; + +/// An [`ElementMap`] for [`FrozenGraph`] edges. +pub type FrozenGraphEdgeMap = ElementMap; + +/// An [`Incidence`] for [`FrozenGraph`]. +pub type FrozenGraphIncidence = Incidence; + +#[derive(Copy, Clone)] +struct IncidenceEntry { + adjacent: Vertex, + opposite: usize, +} + +/// A resumable cursor over the incidences of a single vertex in a [`FrozenGraph`]. +/// +/// Obtain via [`FrozenGraph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance. +#[derive(Copy, Clone)] +pub struct FrozenGraphIncidenceCursor { + current: usize, + end: usize, +} + +impl IncidenceCursor for FrozenGraphIncidenceCursor { + fn next(&mut self, graph: &FrozenGraph) -> Option { + if self.current < self.end { + let incidence = graph.normalize_incidence(self.current); + self.current += 1; + Some(incidence) + } else { + None + } + } +} + +/// An undirected graph that supports only one-time construction, and no further modification. +/// +/// `FrozenGraph` is optimised for workloads that repeatedly query a graph, but do not require +/// changing it once it is constructed. [`Vertex`] and [`Edge`] handles are never invalidated. Use +/// [`AppendGraph`] instead if you need to incrementally add vertices or edges, or [`Graph`] if you +/// need to remove vertices or edges after initial construction. +/// +/// `FrozenGraph` uses a compressed-sparse data representation. Incidences are stored as contiguous +/// adjacency lists per vertex in a single flat [`Vec`]. Vertex neighborhood traversals are +/// therefore cache-friendly without index jumps. +/// +/// # Examples +/// +/// ``` +/// use grapherity::prelude::*; +/// use grapherity::models::{FrozenGraph, AppendGraph}; +/// +/// // Constructs a FrozenGraph instance via AppendGraph. +/// let mut graph = AppendGraph::new(); +/// let v1 = graph.add_vertex(); +/// let v2 = graph.add_vertex(); +/// let e = graph.add_edge(v1, v2); +/// let (graph, vertices, edges) = FrozenGraph::from_graph(&graph); +/// +/// // Queries the FrozenGraph instance. +/// assert!(graph.are_adjacent(vertices[v1], vertices[v2])); +/// ``` +/// +/// # Time and space complexity +/// +/// [`degree`] runs in *O(1)* time. Space complexity is *O(|V| + |E|)*. +/// +/// [`degree`]: Self::degree +/// [`AppendGraph`]: crate::models::AppendGraph +/// [`Graph`]: crate::models::Graph +pub struct FrozenGraph { + vertices: Vec, + incidences: Vec, +} + +impl FrozenGraph { + /// Creates a graph instance as a copy of another graph. + /// + /// Returns the copied graph instance and two mappings of the `source` graph vertices and edges + /// to the new ones. Note that the returned mappings cannot support additional growth, i.e. any + /// vertex or edge added to the `source` graph after calling `from_graph` will map to an invalid + /// vertex or edge in the new `FrozenGraph`. + pub fn from_graph( + source: &G, + ) -> ( + Self, + ElementMap, + ElementMap, + ) { + // Builds vertex list with sentinel and vertex map. + let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); + let mut vertices = Vec::with_capacity(source.vertex_count() + 1); + let mut offset = 0; + for (i, v) in source.vertices().enumerate() { + vertex_map[v] = Vertex(i); + vertices.push(offset); + offset += source.degree(v); + } + vertices.push(offset); + + // Builds incidence list and edge map. Uses "usize::MAX" as "unset" here: an edge can never + // be normalized to "usize::MAX", since it must be the smaller index of two incidences. + const UNSET: usize = usize::MAX; + let mut edge_map = source.edge_map(Edge(UNSET)); + let mut incidences = Vec::with_capacity(source.edge_count() * 2); + for v in source.vertices() { + for Incidence { vertex: u, edge: e } in source.incidences(v) { + let em = edge_map.index_mut(e); + if em.0 == UNSET { + // Adds the first incidence for the edge, but we don't know the opposite, yet. + *em = Edge(incidences.len()); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: 0, + }); + } else { + // Adds the second incidence for the edge and fills the opposite for the first. + incidences[em.0].opposite = incidences.len(); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: em.0, + }); + } + } + } + + ( + Self { + vertices, + incidences, + }, + vertex_map, + edge_map, + ) + } + + fn raw_incidences(&self, v: Vertex) -> impl Iterator { + self.vertices[v.0]..self.vertices[v.0 + 1] + } + + fn normalize_edge(&self, e: usize) -> Edge { + let f = self.incidences[e].opposite; + Edge(if e < f { e } else { f }) + } + + fn normalize_incidence(&self, i: usize) -> FrozenGraphIncidence { + let entry = &self.incidences[i]; + Incidence { + vertex: entry.adjacent, + edge: Edge(if i < entry.opposite { + i + } else { + entry.opposite + }), + } + } +} + +impl From<&G> for FrozenGraph { + fn from(graph: &G) -> Self { + Self::from_graph(graph).0 + } +} + +impl GraphTopology for FrozenGraph { + type Vertex = Vertex; + type Edge = Edge; + type IncidenceCursor = FrozenGraphIncidenceCursor; + + fn vertex_count(&self) -> usize { + self.vertices.len() - 1 + } + + fn vertex_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |v| v.0, self.vertex_count()) + } + + fn edge_count(&self) -> usize { + self.incidences.len() / 2 + } + + fn edge_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |e| e.0, self.edge_count()) + } + + fn degree(&self, v: Self::Vertex) -> usize { + self.vertices[v.0 + 1] - self.vertices[v.0] + } + + fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool { + self.adjacent_vertices(v1).any(|x| x == v2) + } + + fn vertices(&self) -> impl Iterator { + (0..self.vertices.len() - 1).map(Vertex) + } + + fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.incidences[i].adjacent) + } + + fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { + let entry = &self.incidences[e.0]; + (self.incidences[entry.opposite].adjacent, entry.adjacent) + } + + fn edges(&self) -> impl Iterator { + self.incidences + .iter() + .enumerate() + .filter_map(|(i, x)| (i < x.opposite).then_some(Edge(i))) + } + + fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.normalize_edge(i)) + } + + fn incidences(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.normalize_incidence(i)) + } + + fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { + FrozenGraphIncidenceCursor { + current: self.vertices[v.0], + end: self.vertices[v.0 + 1], + } + } +}