Skip to content

Commit

Permalink
Add WCC algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
jvolmer committed Jan 4, 2025
1 parent e13cd67 commit f49b278
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 29 deletions.
88 changes: 88 additions & 0 deletions src/algorithms/wcc/algorithm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::{Component, Graph};

use super::union_find::UnionFind;

pub struct WCC<'a> {
graph: &'a Graph,
union_find: UnionFind,
}

impl<'a> WCC<'a> {
pub fn on(graph: &'a Graph) -> Self {
Self {
graph,
union_find: UnionFind::new(graph.vertices().map(|v| v.clone())),
}
}
pub fn get(self) -> impl Iterator<Item = Component> {
let mut union_find = self.union_find;
self.graph
.edges()
.map(|e| (e.clone().0, e.1.clone()))
.for_each(|(from, to)| union_find.union(from, to).unwrap());
union_find.all_components()
}
}

#[cfg(test)]
mod tests {
use crate::{Graph, VertexId};

use super::*;

#[test]
fn empty_graph_has_no_component() {
let graph = Graph::from(0, vec![]).unwrap();
assert_eq!(
WCC::on(&graph).get().collect::<Vec<_>>(),
Vec::<Component>::new()
);
}

#[test]
fn single_vertex_is_a_weak_component() {
let graph = Graph::from(1, vec![]).unwrap();
assert_eq!(
WCC::on(&graph).get().collect::<Vec<_>>(),
vec![Component::from(vec![VertexId(0)])]
);
}

#[test]
fn unconnected_vertices_are_in_separate_components() {
let graph = Graph::from(2, vec![]).unwrap();
let wcc = WCC::on(&graph).get().collect::<Vec<_>>();
assert_eq!(wcc.len(), 2);
assert!(wcc.contains(&Component::from(vec![VertexId(0)])));
assert!(wcc.contains(&Component::from(vec![VertexId(1)])));
}

#[test]
fn connected_vertices_are_in_same_components_indepent_of_edge_direction() {
let graph = Graph::from(2, vec![(0, 1)]).unwrap();
assert_eq!(
WCC::on(&graph).get().collect::<Vec<_>>(),
vec![Component::from(vec![VertexId(0), VertexId(1)]),]
);

let graph = Graph::from(2, vec![(1, 0)]).unwrap();
assert_eq!(
WCC::on(&graph).get().collect::<Vec<_>>(),
vec![Component::from(vec![VertexId(1), VertexId(0)]),]
);
}

#[test]
fn finds_components_on_some_random_graph() {
let graph = Graph::from(6, vec![(0, 1), (1, 0), (2, 3), (3, 4), (5, 2), (2, 5)]).unwrap();
let wcc = WCC::on(&graph).get().collect::<Vec<_>>();
assert_eq!(wcc.len(), 2);
assert!(wcc.contains(&Component::from(vec![VertexId(0), VertexId(1)])));
assert!(wcc.contains(&Component::from(vec![
VertexId(2),
VertexId(3),
VertexId(4),
VertexId(5)
])));
}
}
1 change: 1 addition & 0 deletions src/algorithms/wcc/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod algorithm;
pub mod union_find;
75 changes: 46 additions & 29 deletions src/algorithms/wcc/union_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,54 +8,37 @@
///! Another improvement that could be implemented: path compression in find fn
use std::collections::HashMap;

use crate::graph::VertexId;

#[derive(Debug, PartialEq, Clone)]
enum Node {
TreeRoot(usize),
DecendentOf(VertexId),
}
use crate::{algorithms::component::Component, graph::VertexId};

#[derive(Debug, PartialEq)]
struct Component {
id: VertexId,
size: usize,
}

#[derive(Debug, PartialEq)]
struct UnionFind {
pub struct UnionFind {
list: HashMap<VertexId, Node>,
}
#[derive(Debug, PartialEq)]
enum Error {
VertexNotIncluded(VertexId),
}

impl UnionFind {
fn new(vertices: impl Iterator<Item = VertexId>) -> Self {
Self {
list: HashMap::from_iter(vertices.map(|v| (v.clone(), Node::TreeRoot(1)))),
}
pub fn new(vertices: impl Iterator<Item = VertexId>) -> Self {
let list = HashMap::from_iter(vertices.map(|v| (v.clone(), Node::TreeRoot(1))));
Self { list }
}
fn find(&self, id: VertexId) -> Result<Component, Error> {
fn find(&self, id: VertexId) -> Result<ComponentId, Error> {
let x = self
.list
.get(&id)
.ok_or(Error::VertexNotIncluded(id.clone()))?;
match x.clone() {
Node::TreeRoot(size) => return Ok(Component { id, size }),
Node::TreeRoot(size) => return Ok(ComponentId { id, size }),
Node::DecendentOf(id) => return self.find(id),
}
}

fn union(&mut self, x: VertexId, y: VertexId) -> Result<(), Error> {
pub fn union(&mut self, x: VertexId, y: VertexId) -> Result<(), Error> {
match (self.find(x), self.find(y)) {
(
Ok(Component {
Ok(ComponentId {
id: xroot,
size: xsize,
}),
Ok(Component {
Ok(ComponentId {
id: yroot,
size: ysize,
}),
Expand All @@ -75,6 +58,40 @@ impl UnionFind {
(_, Err(e)) => return Err(e),
}
}

pub fn all_components(&self) -> impl Iterator<Item = Component> {
self.list
.iter()
.map(|(v, _)| (v, self.find(v.clone())))
.fold(
HashMap::new(),
|mut acc: HashMap<VertexId, Component>, (vertex_id, component_id)| {
acc.entry(component_id.unwrap().id)
.and_modify(|component| component.add(vertex_id.clone()))
.or_insert(Component::from(vec![vertex_id.clone()]));
acc
},
)
.into_iter()
.map(|(_, component)| component)
}
}

#[derive(Debug, PartialEq, Clone)]
enum Node {
TreeRoot(usize),
DecendentOf(VertexId),
}

#[derive(Debug, PartialEq)]
struct ComponentId {
pub id: VertexId,
size: usize,
}

#[derive(Debug, PartialEq)]
pub enum Error {
VertexNotIncluded(VertexId),
}

#[cfg(test)]
Expand All @@ -86,7 +103,7 @@ mod tests {
let union_find = UnionFind::new(vec![VertexId(1)].into_iter());
assert_eq!(
union_find.find(VertexId(1)),
Ok(Component {
Ok(ComponentId {
id: VertexId(1),
size: 1
})
Expand All @@ -99,7 +116,7 @@ mod tests {
UnionFind::new(vec![VertexId(1), VertexId(3), VertexId(5), VertexId(2)].into_iter());
assert_eq!(
union_find.find(VertexId(5)),
Ok(Component {
Ok(ComponentId {
id: VertexId(5),
size: 1
})
Expand Down
4 changes: 4 additions & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ impl Graph {
pub fn vertices(&self) -> impl Iterator<Item = &VertexId> {
self.vertices.iter()
}

pub fn edges(&self) -> impl Iterator<Item = &Edge> {
self.edges.iter()
}
}

#[cfg(test)]
Expand Down

0 comments on commit f49b278

Please sign in to comment.