Skip to content

Commit

Permalink
Merge pull request #44 from azizkayumov/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
azizkayumov committed Nov 6, 2023
2 parents 39a3cb0 + 48d4c2a commit b6ecafa
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 93 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
90 changes: 45 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<FindMax> = 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);
}
```
</details>
Expand Down Expand Up @@ -129,13 +128,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:
Expand Down
1 change: 1 addition & 0 deletions benches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
8 changes: 4 additions & 4 deletions benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
150 changes: 127 additions & 23 deletions src/lctree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,60 @@ pub struct LinkCutTree<P: Path> {
}

impl<P: Path> LinkCutTree<P> {
/// Creates a new empty link-cut tree.
#[must_use]
pub fn new() -> Self {
Self {
forest: Forest::new(),
}
}

/// 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<usize> {
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.
Expand Down Expand Up @@ -49,24 +89,67 @@ impl<P: Path> LinkCutTree<P> {
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);
// if access(w) messed with the root of the tree, then v and w are connected:
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;
Expand All @@ -82,15 +165,36 @@ impl<P: Path> LinkCutTree<P> {
}
}

/// 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<FindMax> = 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);
}
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;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
Loading

0 comments on commit b6ecafa

Please sign in to comment.