502 lines
17 KiB
Rust
502 lines
17 KiB
Rust
//! [`Graph`], an undirected graph topology supporting addition and deletion.
|
|
|
|
use std::num::NonZeroUsize;
|
|
use typed_generational_arena::{Arena, Index};
|
|
|
|
use crate::maps::ElementMap;
|
|
use crate::models;
|
|
use crate::traits::{
|
|
GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion,
|
|
GraphTopologyDeletion, Incidence, 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 [`ElementMap`] for [`Graph`] vertices.
|
|
pub type GraphVertexMap<T> = ElementMap<Vertex, T>;
|
|
|
|
/// An [`ElementMap`] for [`Graph`] edges.
|
|
pub type GraphEdgeMap<T> = ElementMap<Edge, T>;
|
|
|
|
/// An [`Incidence`] for [`Graph`].
|
|
pub type GraphIncidence = Incidence<Vertex, Edge>;
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
struct VertexSlot(usize);
|
|
|
|
// TODO: Benchmark the cost of the index shift. An alternative could be to init Graph::incidences with two dummy entries to avoid 0.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
struct IncidenceSlot(NonZeroUsize);
|
|
|
|
impl IncidenceSlot {
|
|
fn new(index: usize) -> Self {
|
|
IncidenceSlot(NonZeroUsize::new(index + 1).unwrap())
|
|
}
|
|
|
|
fn index(self) -> usize {
|
|
self.0.get() - 1
|
|
}
|
|
}
|
|
|
|
/// Referenced by [`Vertex`] as a type parameter of the underlying arena. Not intended for direct
|
|
/// external use.
|
|
#[doc(hidden)]
|
|
#[derive(Clone)]
|
|
pub struct VertexIncidenceHeader {
|
|
incidence_count: usize,
|
|
first_incidence: Option<IncidenceSlot>,
|
|
}
|
|
|
|
/// Referenced by [`Edge`] 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>,
|
|
adjacent: VertexSlot,
|
|
}
|
|
|
|
struct IncidentEdgeCursor {
|
|
incidence: Option<IncidenceSlot>,
|
|
}
|
|
|
|
impl IncidentEdgeCursor {
|
|
fn next(&mut self, graph: &Graph) -> Option<Edge> {
|
|
graph.step_incidence(&mut self.incidence).map(|(_, e)| e)
|
|
}
|
|
}
|
|
|
|
/// 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>,
|
|
}
|
|
|
|
impl IncidenceCursor<Graph> for GraphIncidenceCursor {
|
|
fn next(&mut self, graph: &Graph) -> Option<GraphIncidence> {
|
|
graph
|
|
.step_incidence(&mut self.incidence)
|
|
.map(|(vs, e)| Incidence {
|
|
vertex: graph.vertices.get_idx(vs.0).unwrap(),
|
|
edge: 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.
|
|
///
|
|
/// # Cloning
|
|
///
|
|
/// [`Vertex`] and [`Edge`] handles obtained from a clone are valid, and refer to the same vertex or
|
|
/// edge in the original, and vice versa. However, once cloned, the two instances no longer affect
|
|
/// each other.
|
|
///
|
|
/// [`add_vertex`]: Self::add_vertex
|
|
/// [`add_edge`]: Self::add_edge
|
|
/// [`degree`]: Self::degree
|
|
/// [`delete_vertex`]: Self::delete_vertex
|
|
/// [`delete_edge`]: Self::delete_edge
|
|
/// [`AppendGraph`]: models::AppendGraph
|
|
#[derive(Clone)]
|
|
pub struct Graph {
|
|
// TODO: Arena index and generation types could be externalized to Graph.
|
|
vertices: Arena<VertexIncidenceHeader, usize, usize>,
|
|
incidences: Arena<IncidenceEntry, usize, usize>,
|
|
}
|
|
|
|
impl Graph {
|
|
/// Creates an empty graph instance with no vertices or edges.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
vertices: Arena::new(),
|
|
incidences: Arena::new(),
|
|
}
|
|
}
|
|
|
|
/// 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, u: Vertex, v: Vertex) -> Edge {
|
|
let edge = self.incidences.insert(IncidenceEntry {
|
|
next: self.vertices[u].first_incidence.take(),
|
|
adjacent: VertexSlot(v.arr_idx()),
|
|
});
|
|
self.vertices[u].incidence_count += 1;
|
|
self.vertices[u].first_incidence = Some(IncidenceSlot::new(edge.arr_idx()));
|
|
edge
|
|
}
|
|
|
|
fn remove_incidence_pair(&mut self, e: Edge) -> (Edge, IncidenceEntry, IncidenceEntry) {
|
|
let f = self
|
|
.incidences
|
|
.get_idx(e.arr_idx() ^ 1)
|
|
.expect("invalid paired incidence index, corrupt internal data state");
|
|
let e_entry = self
|
|
.incidences
|
|
.remove(e)
|
|
.expect("attempt to delete an invalid edge");
|
|
let f_entry = self
|
|
.incidences
|
|
.remove(f)
|
|
.expect("cannot read paired incidence, corrupt internal data state");
|
|
(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.
|
|
fn update_incidence_list(
|
|
&mut self,
|
|
e: Edge,
|
|
source: VertexSlot,
|
|
next: Option<IncidenceSlot>,
|
|
is_loop: bool,
|
|
) {
|
|
let source_vertex = self
|
|
.vertices
|
|
.get_idx(source.0)
|
|
.expect("missing incident vertex, corrupt internal data state");
|
|
let vertex_header = &mut self.vertices[source_vertex];
|
|
vertex_header.incidence_count -= if is_loop { 2 } else { 1 };
|
|
let first = vertex_header
|
|
.first_incidence
|
|
.expect("incident vertex without incidences, corrupt internal data state");
|
|
if first.index() == e.arr_idx() {
|
|
vertex_header.first_incidence = next;
|
|
} else {
|
|
let graph: &Graph = self;
|
|
let (_, previous) = self
|
|
.raw_incidences(source_vertex)
|
|
.find(|(_, f)| {
|
|
graph.incidences[*f]
|
|
.next
|
|
.is_some_and(|i| i.index() == e.arr_idx())
|
|
})
|
|
.expect("cannot find previous incidence, corrupt internal data state");
|
|
self.incidences[previous].next = next;
|
|
}
|
|
}
|
|
|
|
fn raw_incidences(&self, v: Vertex) -> impl Iterator<Item = (VertexSlot, Edge)> {
|
|
let mut incidence = self.vertices[v].first_incidence;
|
|
std::iter::from_fn(move || self.step_incidence(&mut incidence))
|
|
}
|
|
|
|
fn step_incidence(&self, incidence: &mut Option<IncidenceSlot>) -> Option<(VertexSlot, Edge)> {
|
|
// TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries).
|
|
let current = (*incidence)?;
|
|
let e = self.incidences.get_idx(current.index()).unwrap();
|
|
let entry = self.incidences[e];
|
|
*incidence = entry.next;
|
|
Some((entry.adjacent, e))
|
|
}
|
|
|
|
fn normalize_edge(&self, e: Edge) -> Edge {
|
|
let i = e.arr_idx();
|
|
if i & 1 == 0 {
|
|
e
|
|
} else {
|
|
self.incidences.get_idx(i ^ 1).unwrap()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Graph {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl GraphTopology for Graph {
|
|
type Vertex = Vertex;
|
|
type Edge = Edge;
|
|
type IncidenceCursor = GraphIncidenceCursor;
|
|
|
|
fn vertex_count(&self) -> usize {
|
|
self.vertices.len()
|
|
}
|
|
|
|
fn vertex_map<T: Clone>(&self, default: T) -> ElementMap<Self::Vertex, T> {
|
|
ElementMap::new(default, |v| v.arr_idx(), self.vertex_capacity())
|
|
}
|
|
|
|
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.arr_idx() / 2, self.edge_capacity())
|
|
}
|
|
|
|
fn degree(&self, v: Self::Vertex) -> usize {
|
|
self.vertices[v].incidence_count
|
|
}
|
|
|
|
fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool {
|
|
self.adjacent_vertices(u).any(|x| x == v)
|
|
}
|
|
|
|
fn vertices(&self) -> impl Iterator<Item = Self::Vertex> {
|
|
self.vertices.iter().map(|(i, _)| i)
|
|
}
|
|
|
|
fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Vertex> {
|
|
self.raw_incidences(v)
|
|
.map(|(vs, _)| self.vertices.get_idx(vs.0).unwrap())
|
|
}
|
|
|
|
fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) {
|
|
let v = self
|
|
.vertices
|
|
.get_idx(self.incidences[e].adjacent.0)
|
|
.unwrap();
|
|
let f = self.incidences.get_idx(e.arr_idx() ^ 1).unwrap();
|
|
let u = self
|
|
.vertices
|
|
.get_idx(self.incidences[f].adjacent.0)
|
|
.unwrap();
|
|
(u, v)
|
|
}
|
|
|
|
fn edges(&self) -> impl Iterator<Item = Self::Edge> {
|
|
self.incidences
|
|
.iter()
|
|
.filter(|(i, _)| i.arr_idx() % 2 == 0)
|
|
.map(|(i, _)| i)
|
|
}
|
|
|
|
fn incident_edges(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Edge> {
|
|
self.raw_incidences(v).map(|(_, e)| self.normalize_edge(e))
|
|
}
|
|
|
|
fn incidences(&self, v: Self::Vertex) -> impl Iterator<Item = GraphIncidence> {
|
|
self.raw_incidences(v).map(|(vs, e)| Incidence {
|
|
vertex: self.vertices.get_idx(vs.0).unwrap(),
|
|
edge: self.normalize_edge(e),
|
|
})
|
|
}
|
|
|
|
fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor {
|
|
GraphIncidenceCursor {
|
|
incidence: self.vertices[v].first_incidence,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl GraphTopologyAddition for Graph {
|
|
fn vertex_capacity(&self) -> usize {
|
|
self.vertices.capacity()
|
|
}
|
|
|
|
fn edge_capacity(&self) -> usize {
|
|
self.incidences.capacity() / 2
|
|
}
|
|
|
|
fn reserve_vertices(&mut self, additional: usize) {
|
|
self.vertices.reserve(additional);
|
|
}
|
|
|
|
fn reserve_edges(&mut self, additional: usize) {
|
|
self.incidences.reserve(additional * 2);
|
|
}
|
|
|
|
fn add_vertex(&mut self) -> Self::Vertex {
|
|
self.vertices.insert(VertexIncidenceHeader {
|
|
incidence_count: 0,
|
|
first_incidence: None,
|
|
})
|
|
}
|
|
|
|
fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge {
|
|
let first = self.add_incidence(u, v);
|
|
self.add_incidence(v, u);
|
|
first
|
|
}
|
|
}
|
|
|
|
// TODO: Benchmark delete with storing "previous" in O(1) vs. linear lookup in O(degree).
|
|
impl GraphTopologyDeletion for Graph {
|
|
fn delete_vertex(&mut self, v: Self::Vertex) {
|
|
let v_header = self
|
|
.vertices
|
|
.remove(v)
|
|
.expect("attempt to delete an invalid vertex");
|
|
let mut cursor = IncidentEdgeCursor {
|
|
incidence: v_header.first_incidence,
|
|
};
|
|
while let Some(e) = cursor.next(self) {
|
|
// Since v is being deleted, there are no update_incidence_list() calls for e, no need
|
|
// to fix v's incidence list.
|
|
let (f, e_entry, f_entry) = self.remove_incidence_pair(e);
|
|
if e_entry.adjacent != f_entry.adjacent {
|
|
self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false);
|
|
} else {
|
|
cursor.incidence = f_entry.next;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false);
|
|
} else if f_entry.next.is_some_and(|i| e.arr_idx() == i.index()) {
|
|
self.update_incidence_list(f, e_entry.adjacent, e_entry.next, true);
|
|
} else {
|
|
self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl GraphTopologyConversion for Graph {
|
|
fn convert<G: GraphTopology>(source: &G) -> (Self, GraphIsomorphism<G, Self>) {
|
|
let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX));
|
|
let (result, vertex_map) =
|
|
models::convert_to_addition(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| {
|
|
edge_map[e] = new_e
|
|
});
|
|
let iso = GraphIsomorphism::new(&result, vertex_map, edge_map);
|
|
(result, iso)
|
|
}
|
|
}
|
|
|
|
impl<H: GraphTopology> From<&H> for Graph {
|
|
fn from(source: &H) -> Self {
|
|
models::convert_to_addition::<Self, _>(
|
|
source,
|
|
Vertex::new(usize::MAX, usize::MAX),
|
|
|_, _| {},
|
|
)
|
|
.0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod trait_tests {
|
|
use super::Graph;
|
|
use crate::models::AppendGraph;
|
|
|
|
crate::graph_topology_tests!(Graph);
|
|
crate::graph_topology_addition_tests!(Graph);
|
|
crate::graph_topology_deletion_tests!(Graph);
|
|
crate::graph_topology_addition_deletion_tests!(Graph);
|
|
crate::graph_topology_clone_tests!(Graph);
|
|
crate::graph_topology_addition_clone_tests!(Graph);
|
|
crate::graph_topology_conversion_tests!(Graph, AppendGraph);
|
|
crate::graph_topology_addition_conversion_tests!(Graph, AppendGraph);
|
|
crate::from_tests!(Graph, AppendGraph);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::testing::fixtures::MakeTestGraph;
|
|
|
|
#[test]
|
|
fn incident_vertices_paired_index() {
|
|
let (graph, [v0, v1], e) = Graph::single_edge();
|
|
let f = graph
|
|
.incidences
|
|
.get_idx(e.arr_idx() + 1)
|
|
.expect("paired index should be valid");
|
|
let (u0, u1) = graph.incident_vertices(f);
|
|
assert!(
|
|
(u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0),
|
|
"unexpected incident vertices {u0:?} and {u1:?} for edge {f:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_edge_paired_index() {
|
|
let (mut graph, _, e) = Graph::single_edge();
|
|
let f = graph
|
|
.incidences
|
|
.get_idx(e.arr_idx() + 1)
|
|
.expect("paired index should be valid");
|
|
graph.delete_edge(f);
|
|
assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete");
|
|
}
|
|
|
|
#[test]
|
|
fn delete_edge_loop_paired_index() {
|
|
let (mut graph, _, e) = Graph::loop_edge();
|
|
let f = graph
|
|
.incidences
|
|
.get_idx(e.arr_idx() + 1)
|
|
.expect("paired index should be valid");
|
|
graph.delete_edge(f);
|
|
assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete");
|
|
}
|
|
|
|
#[test]
|
|
fn reused_slot_returns_old_value() {
|
|
let (mut graph, [_, v], _) = Graph::single_edge();
|
|
let mut map = graph.vertex_map(0);
|
|
map[v] = 99;
|
|
graph.delete_vertex(v);
|
|
let u = graph.add_vertex();
|
|
assert_eq!(
|
|
v.arr_idx(),
|
|
u.arr_idx(),
|
|
"precondition: new vertex {v:?} should reuse slot of deleted vertex {u:?}"
|
|
);
|
|
// ElementMap uses raw indices, not vertex identity. A new vertex u reusing the slot
|
|
// of previously deleted v sees the old value. Callers must reinitialize stale slots
|
|
// after deletion.
|
|
assert_eq!(
|
|
map[u], 99,
|
|
"new vertex reusing slot of deleted vertex should return old value"
|
|
);
|
|
}
|
|
}
|