Add FrozenGraph (without tests)

This commit is contained in:
2026-08-17 10:25:52 +02:00
parent 5085b7795b
commit a89086253a
5 changed files with 270 additions and 9 deletions
+12 -5
View File
@@ -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
+4 -2
View File
@@ -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};
+3 -1
View File
@@ -67,7 +67,8 @@ impl IncidenceCursor<AppendGraph> 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<AppendGraph> 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<VertexIncidenceHeader>,
+250
View File
@@ -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<T> = ElementMap<Vertex, T>;
/// An [`ElementMap`] for [`FrozenGraph`] edges.
pub type FrozenGraphEdgeMap<T> = ElementMap<Edge, T>;
/// An [`Incidence`] for [`FrozenGraph`].
pub type FrozenGraphIncidence = Incidence<Vertex, Edge>;
#[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<FrozenGraph> for FrozenGraphIncidenceCursor {
fn next(&mut self, graph: &FrozenGraph) -> Option<FrozenGraphIncidence> {
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<usize>,
incidences: Vec<IncidenceEntry>,
}
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<G: GraphTopology>(
source: &G,
) -> (
Self,
ElementMap<G::Vertex, Vertex>,
ElementMap<G::Edge, Edge>,
) {
// 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<Item = usize> {
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<G: GraphTopology> 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<T: Clone>(&self, default: T) -> ElementMap<Self::Vertex, T> {
ElementMap::new(default, |v| v.0, self.vertex_count())
}
fn edge_count(&self) -> usize {
self.incidences.len() / 2
}
fn edge_map<T: Clone>(&self, default: T) -> ElementMap<Self::Edge, T> {
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<Item = Self::Vertex> {
(0..self.vertices.len() - 1).map(Vertex)
}
fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Vertex> {
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<Item = Self::Edge> {
self.incidences
.iter()
.enumerate()
.filter_map(|(i, x)| (i < x.opposite).then_some(Edge(i)))
}
fn incident_edges(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Edge> {
self.raw_incidences(v).map(|i| self.normalize_edge(i))
}
fn incidences(&self, v: Self::Vertex) -> impl Iterator<Item = FrozenGraphIncidence> {
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],
}
}
}