From cb9af4ae0ce1060502f8a3635f6adfafcc768f2a Mon Sep 17 00:00:00 2001 From: Aziz Kayumov Date: Mon, 6 Nov 2023 14:49:30 +0900 Subject: [PATCH 1/2] closes #38 --- Cargo.toml | 2 +- README.md | 3 +- benches/README.md | 1 + benches/benchmark.rs | 8 +-- src/lctree.rs | 150 ++++++++++++++++++++++++++++++++++++------- src/path.rs | 30 ++++----- tests/test_random.rs | 2 +- 7 files changed, 148 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d031086..5407385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ exclude = ["./github"] [dev-dependencies] rand = "0.8" rand_derive2 = "0.1.21" -criterion = { version = "0.4", features = ["html_reports"] } +criterion = "0.4" [[bench]] name = "benchmark" diff --git a/README.md b/README.md index 7aa86d5..b550613 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,14 @@ fn main() { ## Benchmark The overall running time for performing a number of random operations (`link(v, w)`, `cut(v, w)`, `connected(v, w)` or `findmax(v, w)`) on forests of varying sizes (check benchmark details [here](https://github.com/azizkayumov/lctree/blob/main/benches/README.md)). -| # Nodes | # Operations | [lctree](https://github.com/azizkayumov/lctree/blob/main/src/lctree.rs) | [brute-force](https://github.com/azizkayumov/lctree/blob/main/benches/benchmark.rs) | +| # Nodes | # Operations | [lctree](https://github.com/azizkayumov/lctree/blob/main/src/lctree.rs) | [brute-force](https://github.com/azizkayumov/lctree/blob/main/benches/benchmark.rs) | | :--- | :--- | :--- | :--- | | 100 | 10K | 4.8161 ms | 18.013 ms | | 200 | 20K | 11.091 ms | 69.855 ms | | 500 | 50K | 31.623 ms | 429.53 ms | | 1000 | 100K | 68.649 ms | 1.8746 s | | 5000 | 500K | 445.83 ms | 46.854 s | +| 10K | 1M | 964.64 ms | 183.24 s | ## Credits This crate applies the core concepts and ideas presented in the following sources: diff --git a/benches/README.md b/benches/README.md index cd8422d..3711c05 100644 --- a/benches/README.md +++ b/benches/README.md @@ -8,6 +8,7 @@ The overall running time for performing a number of random operations (`link(v, | 500 | 50K | 2 | 31.623 ms | 429.53 ms | | 1000 | 100K | 3 | 68.649 ms | 1.8746 s | | 5000 | 500K | 4 | 445.83 ms | 46.854 s | +| 10K | 1M | 5 | 964.64 ms | 183.24 s | The following table includes worst-case time complexity analysis of each operation for the brute-force solution and Link-cut-trees: diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 8471210..9ce9326 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -5,13 +5,13 @@ use rand_derive2::RandGen; use std::collections::{HashMap, HashSet}; fn benchmark(criterion: &mut Criterion) { - let num_nodes = [100, 200, 500, 1000, 5000]; - let num_operations = [10_000, 20_000, 50_000, 100_000, 500_000]; - let seeds: [u64; 5] = [0, 1, 2, 3, 4]; + let num_nodes = [100, 200, 500, 1000, 5000, 10_000]; + let num_operations = [10_000, 20_000, 50_000, 100_000, 500_000, 1_000_000]; + let seeds: [u64; 6] = [0, 1, 2, 3, 4, 6]; // The last two benchmarks are very slow with the brute force, // so we only run smaller samples: - for i in 0..5 { + for i in 0..3 { let mut group = criterion.benchmark_group(format!("forest_{}", num_nodes[i]).as_str()); group.sample_size(10); diff --git a/src/lctree.rs b/src/lctree.rs index 9e27661..ede1d1f 100644 --- a/src/lctree.rs +++ b/src/lctree.rs @@ -8,6 +8,7 @@ pub struct LinkCutTree { } impl LinkCutTree

{ + /// Creates a new empty link-cut tree. #[must_use] pub fn new() -> Self { Self { @@ -15,13 +16,52 @@ impl LinkCutTree

{ } } - /// Creates a new tree with a single node with the given weight. - /// Returns the id of the node. + /// Creates a new tree with a single node with the given weight and returns its id. + /// If possible, reuses the space of a deleted node and returns its id. + /// + /// # Examples + /// ``` + /// use lctree::LinkCutTree; + /// + /// let mut lctree = LinkCutTree::default(); + /// let alice = lctree.make_tree(0.0); + /// let bob = lctree.make_tree(1.0); + /// let clay = lctree.make_tree(2.0); + /// assert_eq!([alice, bob, clay], [0, 1, 2]); + /// + /// // Remove bob's tree from the forest + /// lctree.remove_tree(bob); + /// + /// // Reuse the space of bob's tree (which was removed) to create a new tree: + /// let david = lctree.make_tree(4.0); + /// assert_eq!(david, bob); + /// ``` pub fn make_tree(&mut self, weight: f64) -> usize { self.forest.create_node(weight) } - /// Delete a tree from the forest + /// Extends the forest with n new single-noded trees with the given weights. + /// + /// # Examples + /// + /// ``` + /// use lctree::LinkCutTree; + /// + /// let weights = vec![1.0, 2.0, 3.0]; + /// let mut lctree = LinkCutTree::default(); + /// let trees_ids = lctree.extend_forest(&weights); + /// assert_eq!(trees_ids, vec![0, 1, 2]); + /// ``` + #[must_use] + pub fn extend_forest(&mut self, weights: &[f64]) -> Vec { + weights + .iter() + .map(|&weight| self.make_tree(weight)) + .collect() + } + + /// Delete a tree with a single node with the given id. + /// /// # Panics /// /// Panics if the tree contains more than one node. @@ -49,7 +89,20 @@ impl LinkCutTree

{ self.forest.flip(v); } - /// Checks if v and w are connected in the forest. + /// Checks if two nodes are connected (i.e. in the same tree). + /// + /// # Examples + /// ``` + /// use lctree::LinkCutTree; + /// + /// let mut lctree = LinkCutTree::default(); + /// let alice = lctree.make_tree(0.0); + /// let bob = lctree.make_tree(1.0); + /// assert!(!lctree.connected(alice, bob)); // not connected yet + /// + /// lctree.link(alice, bob); + /// assert!(lctree.connected(alice, bob)); // now connected + /// ``` pub fn connected(&mut self, v: usize, w: usize) -> bool { self.reroot(v); // v is now the root of the tree self.access(w); @@ -57,16 +110,46 @@ impl LinkCutTree

{ self.forest.parent_of(v).is_some() || v == w } - /// Creates a link between two nodes in the forest (where w is the parent of v). + /// Merges two trees into a single tree. + /// + /// # Examples + /// ``` + /// use lctree::LinkCutTree; + /// + /// let mut lctree = LinkCutTree::default(); + /// let alice = lctree.make_tree(0.0); + /// let bob = lctree.make_tree(1.0); + /// let clay = lctree.make_tree(2.0); + /// + /// lctree.link(alice, bob); + /// lctree.link(bob, clay); + /// assert!(lctree.connected(alice, clay)); + /// ``` pub fn link(&mut self, v: usize, w: usize) { if self.connected(v, w) { return; } - // v is the root of its represented tree, so no need to check if it has a left child + // v is the root of its represented tree: self.forest.set_left(v, w); } - /// Cuts the link between nodes v and w (if it exists) + /// Cuts the link between two nodes (if it exists) + /// + /// # Examples + /// ``` + /// use lctree::LinkCutTree; + /// + /// let mut lctree = LinkCutTree::default(); + /// let alice = lctree.make_tree(0.0); + /// let bob = lctree.make_tree(1.0); + /// assert!(!lctree.connected(alice, bob)); // not connected yet + /// + /// lctree.link(alice, bob); + /// assert!(lctree.connected(alice, bob)); // now connected + /// + /// lctree.cut(alice, bob); + /// assert!(!lctree.connected(alice, bob)); // not connected again + /// ``` pub fn cut(&mut self, v: usize, w: usize) { if !self.connected(v, w) { return; @@ -82,7 +165,28 @@ impl LinkCutTree

{ } } - /// Performs path aggregation on a path between v and w (if they are connected) + /// Performs path aggregation on a path between two nodes (if they are connected) + /// + /// # Examples + /// ``` + /// use lctree::{LinkCutTree, FindMax}; + /// + /// let mut lctree: LinkCutTree = LinkCutTree::new(); + /// let alice = lctree.make_tree(0.0); + /// let bob = lctree.make_tree(10.0); + /// let clay = lctree.make_tree(1.0); + /// let dave = lctree.make_tree(2.0); + /// + /// // Form a path from Alice to Dave: + /// lctree.link(alice, bob); + /// lctree.link(bob, clay); + /// lctree.link(clay, dave); + /// + /// // Find the richest guy in the path from Alice to Dave: + /// let richest_guy = lctree.path(alice, dave); + /// assert_eq!(richest_guy.idx, bob); + /// assert_eq!(richest_guy.weight, 10.0); + /// ``` pub fn path(&mut self, v: usize, w: usize) -> P { if !self.connected(v, w) { return P::default(f64::INFINITY, usize::MAX); @@ -90,7 +194,7 @@ impl LinkCutTree

{ self.forest.aggregated_path_of(w) } - /// Finds the root of the tree that v is in. + /// Finds the root of the tree that the query node is in. pub fn findroot(&mut self, v: usize) -> usize { self.access(v); let mut root = v; @@ -314,13 +418,13 @@ mod tests { lctree.link(9, 7); // We check the node index with max weight in the path from each node to the root: - assert_eq!(lctree.path(4, 5).max_weight_idx, 0); - assert_eq!(lctree.path(3, 6).max_weight_idx, 0); - assert_eq!(lctree.path(2, 7).max_weight_idx, 0); - assert_eq!(lctree.path(1, 8).max_weight_idx, 0); - assert_eq!(lctree.path(0, 9).max_weight_idx, 0); - assert_eq!(lctree.path(4, 3).max_weight_idx, 2); - assert_eq!(lctree.path(5, 7).max_weight_idx, 6); + assert_eq!(lctree.path(4, 5).idx, 0); + assert_eq!(lctree.path(3, 6).idx, 0); + assert_eq!(lctree.path(2, 7).idx, 0); + assert_eq!(lctree.path(1, 8).idx, 0); + assert_eq!(lctree.path(0, 9).idx, 0); + assert_eq!(lctree.path(4, 3).idx, 2); + assert_eq!(lctree.path(5, 7).idx, 6); } #[test] @@ -352,13 +456,13 @@ mod tests { lctree.link(9, 7); // We check the node index with max weight in the path from each node to the root: - assert_eq!(lctree.path(4, 5).min_weight_idx, 1); - assert_eq!(lctree.path(3, 6).min_weight_idx, 3); - assert_eq!(lctree.path(2, 7).min_weight_idx, 1); - assert_eq!(lctree.path(1, 8).min_weight_idx, 1); - assert_eq!(lctree.path(0, 9).min_weight_idx, 5); - assert_eq!(lctree.path(4, 3).min_weight_idx, 3); - assert_eq!(lctree.path(5, 7).min_weight_idx, 5); + assert_eq!(lctree.path(4, 5).idx, 1); + assert_eq!(lctree.path(3, 6).idx, 3); + assert_eq!(lctree.path(2, 7).idx, 1); + assert_eq!(lctree.path(1, 8).idx, 1); + assert_eq!(lctree.path(0, 9).idx, 5); + assert_eq!(lctree.path(4, 3).idx, 3); + assert_eq!(lctree.path(5, 7).idx, 5); } #[test] diff --git a/src/path.rs b/src/path.rs index d6f6729..5632db4 100644 --- a/src/path.rs +++ b/src/path.rs @@ -5,44 +5,38 @@ pub trait Path: Copy + Clone { #[derive(Copy, Clone)] pub struct FindMax { - pub max_weight_idx: usize, - pub max_weight: f64, + pub idx: usize, + pub weight: f64, } impl Path for FindMax { fn default(weight: f64, index: usize) -> Self { - FindMax { - max_weight_idx: index, - max_weight: weight, - } + FindMax { idx: index, weight } } fn aggregate(&mut self, other: Self) { - if other.max_weight > self.max_weight { - self.max_weight = other.max_weight; - self.max_weight_idx = other.max_weight_idx; + if other.weight > self.weight { + self.weight = other.weight; + self.idx = other.idx; } } } #[derive(Copy, Clone)] pub struct FindMin { - pub min_weight_idx: usize, - pub min_weight: f64, + pub idx: usize, + pub weight: f64, } impl Path for FindMin { fn default(weight: f64, index: usize) -> Self { - FindMin { - min_weight_idx: index, - min_weight: weight, - } + FindMin { idx: index, weight } } fn aggregate(&mut self, other: Self) { - if other.min_weight < self.min_weight { - self.min_weight = other.min_weight; - self.min_weight_idx = other.min_weight_idx; + if other.weight < self.weight { + self.weight = other.weight; + self.idx = other.idx; } } } diff --git a/tests/test_random.rs b/tests/test_random.rs index 01c2ce5..71b2c05 100644 --- a/tests/test_random.rs +++ b/tests/test_random.rs @@ -54,7 +54,7 @@ pub fn validation_check() { assert_eq!(actual, expected); } Operation::Path => { - let actual = lctree.path(v, w).max_weight_idx; + let actual = lctree.path(v, w).idx; let expected = brute.findmax(v, w); assert_eq!(actual, expected); } From 48d4c2af807f6ecf087f9077a85c260c674d25eb Mon Sep 17 00:00:00 2001 From: Aziz Kayumov Date: Mon, 6 Nov 2023 15:01:25 +0900 Subject: [PATCH 2/2] update docs --- README.md | 87 ++++++++++++++++++++++---------------------- tests/test_random.rs | 34 ++++++++++++++++- 2 files changed, 76 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b550613..86cfa68 100644 --- a/README.md +++ b/README.md @@ -15,41 +15,39 @@ use lctree::LinkCutTree; fn main() { // We form a link-cut tree from the following rooted tree: - // 0 + // a // / \ - // 1 4 + // b e // / \ \ - // 2 3 5 - // / - // 6 + // c d f let mut lctree = lctree::LinkCutTree::default(); - for i in 0..7 { - lctree.make_tree(i as f64); - } - lctree.link(1, 0); - lctree.link(2, 1); - lctree.link(3, 1); - lctree.link(4, 0); - lctree.link(5, 4); - lctree.link(6, 5); + let a = lctree.make_tree(0.0); + let b = lctree.make_tree(1.0); + let c = lctree.make_tree(2.0); + let d = lctree.make_tree(3.0); + let e = lctree.make_tree(4.0); + let f = lctree.make_tree(5.0); + lctree.link(b, a); + lctree.link(c, b); + lctree.link(d, b); + lctree.link(e, a); + lctree.link(f, e); // Checking connectivity: - assert!(lctree.connected(2, 6)); // connected + assert!(lctree.connected(c, f)); // connected - // We cut node 4 from its parent 0: - lctree.cut(4, 0); + // We cut node e from its parent a: + lctree.cut(e, a); // The forest should now look like this: - // 0 - // / - // 1 4 + // a + // / + // b e // / \ \ - // 2 3 5 - // / - // 6 + // c d f // We check connectivity again: - assert!(!lctree.connected(2, 6)); // not connected anymore + assert!(!lctree.connected(c, f)); // not connected anymore } ``` Advanced usage include operations on paths: @@ -64,31 +62,32 @@ use lctree::{LinkCutTree, FindMax, FindMin, FindSum}; fn main() { // We form a link-cut tree from the following rooted tree // (the numbers in parentheses are the weights of the nodes): - // 0(9) + // a(9) // / \ - // 1(1) 4(2) + // b(1) e(2) // / \ \ - // 2(8) 3(0) 5(4) - // / - // 6(3) + // c(8) d(0) f(4) // Replace FindMax with FindMin or FindSum, depending on your usage: let mut lctree: LinkCutTree = lctree::LinkCutTree::new(); - let weights = [9.0, 1.0, 8.0, 0.0, 2.0, 4.0, 3.0]; - for i in 0..weights.len() { - lctree.make_tree(weights[i]); - } - lctree.link(1, 0); - lctree.link(2, 1); - lctree.link(3, 1); - lctree.link(4, 0); - lctree.link(5, 4); - lctree.link(6, 5); - - // We find the node with max weight on the path between 2 to 6, - // where 0 has the maximum weight of 9.0: - assert_eq!(lctree.path(2, 6).max_weight, 9.0); - assert_eq!(lctree.path(2, 6).max_weight_idx, 0); + let a = lctree.make_tree(9.); + let b = lctree.make_tree(1.); + let c = lctree.make_tree(8.); + let d = lctree.make_tree(0.); + let e = lctree.make_tree(2.); + let f = lctree.make_tree(4.); + + lctree.link(b, a); + lctree.link(c, b); + lctree.link(d, b); + lctree.link(e, a); + lctree.link(f, e); + + // We find the node with max weight on the path between c to f, + // where a has the maximum weight of 9.0: + let heaviest_node = lctree.path(c, f); + assert_eq!(heaviest_node.idx, a); + assert_eq!(heaviest_node.weight, 9.0); } ``` diff --git a/tests/test_random.rs b/tests/test_random.rs index 71b2c05..7a0fb7c 100644 --- a/tests/test_random.rs +++ b/tests/test_random.rs @@ -1,8 +1,40 @@ -use lctree::LinkCutTree; +use lctree::{FindMax, LinkCutTree}; use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng}; use rand_derive2::RandGen; use std::collections::{HashMap, HashSet}; +#[test] +pub fn intro() { + // We form a link-cut tree from the following rooted tree + // (the numbers in parentheses are the weights of the nodes): + // a(9) + // / \ + // b(1) e(2) + // / \ \ + // c(8) d(0) f(4) + + // Replace FindMax with FindMin or FindSum, depending on your usage: + let mut lctree: LinkCutTree = lctree::LinkCutTree::new(); + let a = lctree.make_tree(9.); + let b = lctree.make_tree(1.); + let c = lctree.make_tree(8.); + let d = lctree.make_tree(0.); + let e = lctree.make_tree(2.); + let f = lctree.make_tree(4.); + + lctree.link(b, a); + lctree.link(c, b); + lctree.link(d, b); + lctree.link(e, a); + lctree.link(f, e); + + // We find the node with max weight on the path between c to f, + // where a has the maximum weight of 9.0: + let heaviest_node = lctree.path(c, f); + assert_eq!(heaviest_node.idx, a); + assert_eq!(heaviest_node.weight, 9.0); +} + #[test] pub fn validation_check() { // These can be larger if you have time to spare (see tests/README.md)