378: Graph — Adjacency Matrix
Tutorial Video
Text description (accessibility)
This video demonstrates the "378: Graph — Adjacency Matrix" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Some graph algorithms require O(1) edge existence queries. Key difference from OCaml: 1. **Index syntax**: Rust uses `matrix[u][v]` with bounds
Tutorial
The Problem
Some graph algorithms require O(1) edge existence queries. For dense graphs where most pairs of vertices are connected — circuit boards, protein interaction networks, game adjacency grids — storing edges as a 2D boolean matrix provides constant-time lookup at the cost of O(V²) space. Transitive closure algorithms (Floyd-Warshall), adjacency matrix multiplication for path counting, and spectral graph analysis all operate naturally on matrix form.
Adjacency matrices appear in network routing tables, game AI pathfinding on grid maps, social influence modeling, and scientific computing (graph Laplacian for mesh simulation).
🎯 Learning Outcomes
Vec<Vec<bool>> in Rusthas_edge vs. O(V) neighbor enumerationCode Example
#![allow(clippy::all)]
//! Graph - Adjacency Matrix Representation
//!
//! O(1) edge lookup, O(V²) storage.
/// Graph with adjacency matrix representation
pub struct Graph {
vertices: usize,
matrix: Vec<Vec<bool>>,
}
impl Graph {
pub fn new(n: usize) -> Self {
Self {
vertices: n,
matrix: vec![vec![false; n]; n],
}
}
pub fn add_edge(&mut self, u: usize, v: usize) {
self.matrix[u][v] = true;
self.matrix[v][u] = true;
}
pub fn add_directed_edge(&mut self, u: usize, v: usize) {
self.matrix[u][v] = true;
}
pub fn has_edge(&self, u: usize, v: usize) -> bool {
self.matrix[u][v]
}
pub fn neighbors(&self, u: usize) -> Vec<usize> {
(0..self.vertices).filter(|&v| self.matrix[u][v]).collect()
}
pub fn degree(&self, u: usize) -> usize {
self.neighbors(u).len()
}
pub fn vertex_count(&self) -> usize {
self.vertices
}
pub fn edge_count(&self) -> usize {
let mut count = 0;
for i in 0..self.vertices {
for j in i + 1..self.vertices {
if self.matrix[i][j] {
count += 1;
}
}
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_edge() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert!(g.has_edge(0, 1));
assert!(g.has_edge(1, 0));
assert!(!g.has_edge(0, 3));
}
#[test]
fn test_neighbors() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
g.add_edge(0, 3);
let n = g.neighbors(0);
assert_eq!(n.len(), 3);
}
#[test]
fn test_degree() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert_eq!(g.degree(0), 2);
assert_eq!(g.degree(1), 1);
}
#[test]
fn test_edge_count() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(1, 2);
g.add_edge(2, 3);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn test_directed() {
let mut g = Graph::new(3);
g.add_directed_edge(0, 1);
assert!(g.has_edge(0, 1));
assert!(!g.has_edge(1, 0));
}
}Key Differences
matrix[u][v] with bounds-checked indexing (panics on OOB); OCaml uses matrix.(u).(v) which also raises Invalid_argument on OOB.vec![vec![false; n]; n]; OCaml uses Array.make_matrix n n false — equivalent performance, different ergonomics.(0..n).filter(|&v| self.matrix[u][v]).collect(); OCaml would use Array.to_seqi or a recursive fold.&mut self for edge insertion enforced by the borrow checker; OCaml arrays are mutable by default with no compile-time tracking.OCaml Approach
OCaml uses bool array array for the adjacency matrix, initialized with Array.make_matrix. Edge checking is matrix.(u).(v). Neighbor enumeration uses Array.to_seq with Seq.filter_mapi. The matrix representation integrates naturally with OCaml's array performance, which matches Rust's since both compile to contiguous memory.
Full Source
#![allow(clippy::all)]
//! Graph - Adjacency Matrix Representation
//!
//! O(1) edge lookup, O(V²) storage.
/// Graph with adjacency matrix representation
pub struct Graph {
vertices: usize,
matrix: Vec<Vec<bool>>,
}
impl Graph {
pub fn new(n: usize) -> Self {
Self {
vertices: n,
matrix: vec![vec![false; n]; n],
}
}
pub fn add_edge(&mut self, u: usize, v: usize) {
self.matrix[u][v] = true;
self.matrix[v][u] = true;
}
pub fn add_directed_edge(&mut self, u: usize, v: usize) {
self.matrix[u][v] = true;
}
pub fn has_edge(&self, u: usize, v: usize) -> bool {
self.matrix[u][v]
}
pub fn neighbors(&self, u: usize) -> Vec<usize> {
(0..self.vertices).filter(|&v| self.matrix[u][v]).collect()
}
pub fn degree(&self, u: usize) -> usize {
self.neighbors(u).len()
}
pub fn vertex_count(&self) -> usize {
self.vertices
}
pub fn edge_count(&self) -> usize {
let mut count = 0;
for i in 0..self.vertices {
for j in i + 1..self.vertices {
if self.matrix[i][j] {
count += 1;
}
}
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_edge() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert!(g.has_edge(0, 1));
assert!(g.has_edge(1, 0));
assert!(!g.has_edge(0, 3));
}
#[test]
fn test_neighbors() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
g.add_edge(0, 3);
let n = g.neighbors(0);
assert_eq!(n.len(), 3);
}
#[test]
fn test_degree() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert_eq!(g.degree(0), 2);
assert_eq!(g.degree(1), 1);
}
#[test]
fn test_edge_count() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(1, 2);
g.add_edge(2, 3);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn test_directed() {
let mut g = Graph::new(3);
g.add_directed_edge(0, 1);
assert!(g.has_edge(0, 1));
assert!(!g.has_edge(1, 0));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_edge() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert!(g.has_edge(0, 1));
assert!(g.has_edge(1, 0));
assert!(!g.has_edge(0, 3));
}
#[test]
fn test_neighbors() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
g.add_edge(0, 3);
let n = g.neighbors(0);
assert_eq!(n.len(), 3);
}
#[test]
fn test_degree() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(0, 2);
assert_eq!(g.degree(0), 2);
assert_eq!(g.degree(1), 1);
}
#[test]
fn test_edge_count() {
let mut g = Graph::new(4);
g.add_edge(0, 1);
g.add_edge(1, 2);
g.add_edge(2, 3);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn test_directed() {
let mut g = Graph::new(3);
g.add_directed_edge(0, 1);
assert!(g.has_edge(0, 1));
assert!(!g.has_edge(1, 0));
}
}
Deep Comparison
OCaml vs Rust: Adjacency Matrix
Both use 2D array of booleans or weights.
Exercises
closure[u][v] = true if there is any path from u to v.