Skip to content

Commit

Permalink
v0.2.0 (#25)
Browse files Browse the repository at this point in the history
* 20 convert edge type into a tuple struct (#23)

* Changed Edge (tuple type) to Edge (tuple struct) and made related changes

* Added Edge changes to sync_digraph, ungraph and sync_ungraph

* Changed Path<K, N, E> types iterator to use Edge<K, N, E> instead of (u, v, e)

* Corrected kojarasu examples to be stable for assertion tests

* Readme updated to reflect changes in pr

* Correct incorrect catgory slug in Cargo.toml https://crates.io/category_slugs

* 22 add minimum spanning tree to examples (#24)

* #22 Add prim's algorithm example

* #22 Fix example to work with Edge() tuple struct

* #22 Fix exec method in ungraph to work with Edge

* Add documentation to prim example, refactor

* Improve package description

* More descriptive names for examples

* Make ungraph methods FnMut, simplify prim's example, add for_each to ungraph

* Change map to for_each and remove filter_map from methods

* Improve documentation

* Corret memory leak. Adjacency lists need to store WeakNode references to avoid circular reference

* Improve build.yml to check for leaks on examples with valgrind

* Changed branch in build.yml for testing

* Remove size test for now

* Correct size tests

* Restructure node module in ungraph and digraph, chaneg ungraph to use std's BinaryHeap

* Copy new implementation to sync versions

* Correct documentation

* If node that an edge is pointing to has been dropped, panic

* Change build.yml to point to master branch

Co-authored-by: Satu Koskinen <[email protected]>
  • Loading branch information
juliuskoskela and satukoskinen authored Sep 10, 2022
1 parent f43f31d commit ed6d0ec
Show file tree
Hide file tree
Showing 63 changed files with 2,519 additions and 2,255 deletions.
19 changes: 18 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,24 @@ jobs:

steps:
- uses: actions/checkout@v3

- name: Build
run: cargo build --verbose
run: cargo build --verbose --examples

- name: Run tests
run: cargo test --verbose

- name: Set up environment
run: |
sudo apt update
sudo apt install -y libev-dev valgrind
- name: Run Tests with Valgrind
run: |
touch results
valgrind --leak-check=full --track-origins=yes -q ./target/debug/examples/dijkstra-shortest-path >> results
valgrind --leak-check=full --track-origins=yes -q ./target/debug/examples/serialization-example >> results
valgrind --leak-check=full --track-origins=yes -q ./target/debug/examples/edmonds-karp-maximum-flow >> results
valgrind --leak-check=full --track-origins=yes -q ./target/debug/examples/prim-minimum-spanning-tree >> results
valgrind --leak-check=full --track-origins=yes -q ./target/debug/examples/kojarasu-strongly-connected-components >> results
[ -s ./results ] && (cat ./results && exit -1) || echo "No leaks detected"
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
[package]
name = "gdsl"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
readme = "README.md"
license = "MIT/Apache-2.0"
authors = [ "juliuskoskela" ]
authors = ["Julius Koskela <me@juliuskoskela.dev>"]

description = "Graph Data Structure Library"
description = """
GDSL is a graph data-structure library including graph containers,
connected node strutures and efficient algorithms on those structures.
Nodes are independent of a graph container and can be used as connected
smart pointers.
"""
repository = "https://github.com/juliuskoskela/gdsl"

keywords = ["data-structure", "graph", "algorithms", "containers", "graph-theory"]
categories = ["Data structures"]
categories = ["data-structure", "algorithms", "mathematics", "science"]

[dependencies]
min-max-heap = "1.3.0"
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ assert!(*n1 == 42);
assert!(n2.key() == &'B');

// Get the next edge from the outbound iterator.
let (u, v, e) = n1.iter_out().next().unwrap();
let Edge(u, v, e) = n1.iter_out().next().unwrap();

assert!(u.key() == &'A');
assert!(v == n2);
Expand All @@ -70,12 +70,19 @@ inbound edges in case of a directed graph or adjacent edges in the case of an un
graph.

```rust
for (u, v, e) in &node {

// Edge is a tuple struct so can be decomposed using tuple syntax..
for Edge(u, v, e) in &node {
println!("{} -> {} : {}", u.key(), v.key(), e);
}

// ..or used more conventionally as one type.
for edge in &node {
println!("{} -> {} : {}", edge.source().key(), edge.target().key(), edge.value());
}

// Transposed iteration i.e. iterating the inbound edges of a node in digrap.
for (u, v, e) in node.iter_in() {
for Edge(v, u, e) in node.iter_in() {
println!("{} <- {} : {}", u.key(), v.key(), e);
}
```
Expand Down Expand Up @@ -240,14 +247,14 @@ g['A'].set(0);
// by calling the `pfs()` method on the node.
//
// If we find a shorter distance to a node we are traversing, we need to
// update the distance of the node. We do this by using the `map()` method
// on the PFS search object. The `map()` method takes a closure as argument
// update the distance of the node. We do this by using the `for_each()` method
// on the PFS search object. The `for_each()` method takes a closure as argument
// and calls it for each edge that is traversed. This way we can manipulate
// the distance of the node. based on the edge that is traversed.
//
// The search-object evaluates lazily. This means that the search is only
// executed when calling either `search()` or `search_path()`.
g['A'].pfs().map(&|u, v, e| {
g['A'].pfs().for_each(&mut |Edge(u, v, e)| {

// Since we are using a `Cell` to store the distance we use `get()` to
// read the distance values.
Expand Down
8 changes: 4 additions & 4 deletions examples/dijkstra.rs → examples/dijkstra-shortest-path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

use gdsl::*;
use gdsl::digraph::*;
use std::cell::Cell;

fn main() {
Expand Down Expand Up @@ -45,15 +46,14 @@ fn main() {
// by calling the `pfs()` method on the node.
//
// If we find a shorter distance to a node we are traversing, we need to
// update the distance of the node. We do this by using the `map()` method
// on the PFS search object. The `map()` method takes a closure as argument
// update the distance of the node. We do this by using the `for_each()` method
// on the PFS search object. The `for_each()` method takes a closure as argument
// and calls it for each edge that is traversed. This way we can manipulate
// the distance of the node. based on the edge that is traversed.
//
// The search-object evaluates lazily. This means that the search is only
// executed when calling either `search()` or `search_path()`.
g['A'].pfs().map(&|u, v, e| {

g['A'].pfs().for_each(&mut |Edge(u, v, e)| {
// Since we are using a `Cell` to store the distance we use `get()` to
// read the distance values.
let (u_dist, v_dist) = (u.get(), v.get());
Expand Down
File renamed without changes.
53 changes: 4 additions & 49 deletions examples/edmonds-karp.rs → examples/edmonds-karp-maximum-flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,20 @@ fn max_flow(g: &G) -> u64 {
.dfs()
.target(&5)
// 2. We exclude saturated edges from the search.
.filter(&|_, _, e| e.cur() < e.max())
.filter(&mut |Edge(_, _, e)| e.cur() < e.max())
.search_path()
{
let mut aug_flow = std::u64::MAX;

// 3. We find the minimum augmenting flow along the path.
for (_, _, flow) in path.iter_edges() {
for Edge(_, _, flow) in path.iter_edges() {
if flow.max() - flow.cur() < aug_flow {
aug_flow = flow.max() - flow.cur();
}
}

// 4. We update the flow along the path.
for (_, _, flow) in path.iter_edges() {
for Edge(_, _, flow) in path.iter_edges() {
flow.update(aug_flow);
}

Expand Down Expand Up @@ -118,49 +118,4 @@ fn main() {

// For this Graph we expect the maximum flow from 0 -> 5 to be 23
assert!(max_flow(&g) == 23);

// print_flow_graph(&g);
}

// fn attr(field: &str, value: &str) -> (String, String) {
// (field.to_string(), value.to_string())
// }

// pub const THEME: [&str; 5] = [
// "#ffffff", // 0. background
// "#ffe5a9", // 1. medium
// "#423f3b", // 2. dark
// "#ff6666", // 3. accent
// "#525266", // 4. theme
// ];

// fn print_flow_graph(g: &G) {
// let dot_str = g.to_dot_with_attr(
// &|| {
// Some(vec![
// attr("bgcolor", THEME[0]),
// attr("fontcolor", THEME[4]),
// attr("label", "Flow Graph"),
// ])
// },
// &|node| {
// Some(vec![
// attr("fillcolor", THEME[1]),
// attr("fontcolor", THEME[4]),
// attr("label", &format!("{}", node.key())),
// ])
// },
// &|_, _, edge| {
// let Flow (max, cur) = edge.0.get();
// let flow_str = format!("{}/{}", cur, max);
// let color = if cur == 0 { THEME[4] } else { THEME[3] };
// Some(vec![
// attr("fontcolor", THEME[4]),
// attr("label", &flow_str),
// attr("color", &color),
// ])
// }
// );

// println!("{}", dot_str);
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn ordering(graph: &G) -> G {
if !visited.contains(next.key()) {
let partition = next
.postorder()
.filter(&|_, v, _| !visited.contains(v.key()))
.filter(&mut |Edge(_, v, _)| !visited.contains(v.key()))
.search_nodes();
for node in &partition {
visited.insert(node.key().clone());
Expand All @@ -44,7 +44,7 @@ fn kojarasu(graph: &G) -> Vec<G> {
let cycle = node
.dfs()
.transpose()
.filter(&|_, v, _| !invariant.contains(v.key()))
.filter(&mut |Edge(_, v, _)| !invariant.contains(v.key()))
.search_cycle();
match cycle {
Some(cycle) => {
Expand Down Expand Up @@ -94,7 +94,7 @@ fn ex1() {
];

let mut g = g.to_vec();
g.sort();
g.sort_by(|a, b| a.key().cmp(&b.key()));
let mut components = kojarasu(&g);

for (i, component) in components.iter_mut().enumerate() {
Expand Down Expand Up @@ -135,7 +135,7 @@ fn ex2() {
];

let mut g = g.to_vec();
g.sort();
g.sort_by(|a, b| a.key().cmp(&b.key()));
let mut components = kojarasu(&g);

for (i, component) in components.iter_mut().enumerate() {
Expand Down Expand Up @@ -174,7 +174,7 @@ fn ex3() {
];

let mut g = g.to_vec();
g.sort();
g.sort_by(|a, b| a.key().cmp(&b.key()));
let mut components = kojarasu(&g);

for (i, component) in components.iter_mut().enumerate() {
Expand Down Expand Up @@ -213,7 +213,7 @@ fn ex4() {
];

let mut g = g.to_vec();
g.sort();
g.sort_by(|a, b| a.key().cmp(&b.key()));
let mut components = kojarasu(&g);

for (i, component) in components.iter_mut().enumerate() {
Expand Down
115 changes: 115 additions & 0 deletions examples/prim-minimum-spanning-tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Prim's algorithm
//
// Prim's algorithm (also known as Jarník's algorithm) is a greedy algorithm
// that finds a minimum spanning tree for a weighted undirected graph. This
// means it finds a subset of the edges that forms a tree that includes every
// vertex, where the total weight of all the edges in the tree is minimized.
// The algorithm operates by building this tree one vertex at a time,
// from an arbitrary starting vertex, at each step adding the cheapest possible
// connection from the tree to another vertex.
//
// https://en.wikipedia.org/wiki/Prim%27s_algorithm

use gdsl::ungraph::*;
use gdsl::*;
use std::collections::{BinaryHeap, HashSet};
use std::cmp::Reverse;

type N = Node<usize, (), u64>;
type E = Edge<usize, (), u64>;

// Standard library's BinaryHeap is a max-heap, so we need to reverse the
// ordering of the edge weights to get a min-heap using the Reverse wrapper.
type Heap = BinaryHeap<Reverse<E>>;

fn prim_minimum_spanning_tree(s: &N) -> Vec<E> {

// We collect the resulting MST edges in to a vector.
let mut mst: Vec<E> = vec![];

// We use a HashSet to keep track of the nodes that are in the MST.
let mut in_mst: HashSet<usize> = HashSet::new();

// We use a BinaryHeap to keep track of all edges sorted by weight.
let mut heap = Heap::new();

in_mst.insert(*s.key());

// Collect all edges reachable from `s` to a Min Heap.
s.bfs().for_each(&mut |edge| {
heap.push(Reverse(edge.clone()));
}).search();

// When we pop from the min heap, we know that the edge is the cheapest
// edge to add to the MST, but we need to make sure that the edge
// connecting to a node that is not already in the MST, otherwise we
// we store the edge and continue to the next iteration. When we find
// an edge that connects to a node that is not in the MST, we add the
// stored edges back to the heap.
let mut tmp: Vec<E> = vec![];

// While the heap is not empty, search for the next edge
// that connects a node in the tree to a node not in the tree.
while let Some(Reverse(edge)) = heap.pop() {
let Edge(u, v, _) = edge.clone();

// If the edge's source node `u` is in the MST...
if in_mst.contains(u.key()) {

// ...and the edge's destination node `v` is not in the MST,
// then we add the edge to the MST and add all edges
// in `tmp` back to the heap.
if in_mst.contains(v.key()) == false {
in_mst.insert(*v.key());
mst.push(edge.clone());
for tmp_edge in &tmp {
heap.push(Reverse(tmp_edge.clone()));
}
}
} else {

// The edge is the cheapest edge to add to the MST, but
// it's source node `u` nor it's destination node `v` are
// in the MST, so we store the edge and continue to the next
// iteration.
if in_mst.contains(v.key()) == false {
tmp.push(edge);
}
}

// If neither condition is met, then the edge's destination node
// `v` is already in the MST, so we continue to the next iteration.
}
mst
}

fn main() {
// Example g1 from Wikipedia
let g1 = ungraph![
(usize) => [u64]
(0) => [ (1, 1), (3, 4), (4, 3)]
(1) => [ (3, 4), (4, 2)]
(2) => [ (4, 4), (5, 5)]
(3) => [ (4, 4)]
(4) => [ (5, 7)]
(5) => []
];
let forest = prim_minimum_spanning_tree(&g1[0]);
let sum = forest.iter().fold(0, |acc, e| acc + e.2);
assert!(sum == 16);

// Example g2 from Figure 7.1 in https://jeffe.cs.illinois.edu/teaching/algorithms/book/07-mst.pdf
let g2 = ungraph![
(usize) => [u64]
(0) => [ (1, 8), (2, 5)]
(1) => [ (2, 10), (3, 2), (4, 18)]
(2) => [ (3, 3), (5, 16)]
(3) => [ (4, 12), (5, 30)]
(4) => [ (6, 4)]
(5) => [ (6, 26)]
(6) => []
];
let forest = prim_minimum_spanning_tree(&g2[0]);
let sum = forest.iter().fold(0, |acc, e| acc + e.2);
assert!(sum == 42);
}
2 changes: 1 addition & 1 deletion examples/serde.rs → examples/serialization-example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn main() {

for (a, b) in graph_cbor_vec.iter().zip(graph_json_vec.iter()) {
assert!(a == b);
for ((u, v, e), (uu, vv, ee)) in a.iter_out().zip(b.iter_out()) {
for (Edge(u, v, e), Edge(uu, vv, ee)) in a.iter_out().zip(b.iter_out()) {
assert!(u == uu);
assert!(v == vv);
assert!(e == ee);
Expand Down
Loading

0 comments on commit ed6d0ec

Please sign in to comment.