Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #44

Merged
merged 2 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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