Skip to content

Commit

Permalink
closes #36
Browse files Browse the repository at this point in the history
  • Loading branch information
azizkayumov committed Nov 2, 2023
1 parent fbdd462 commit 35fc75a
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 71 deletions.
5 changes: 3 additions & 2 deletions src/lctree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ impl<T: Path> LinkCutTree<T> {
}
// detach w from its parent (which is v)
if let Some(left) = self.forest[w].left {
if left != v {
eprintln!("Error: no link between {v} and {w}"); // maybe this should be a panic?
if left != v || self.forest[v].right.is_some() {
// maybe this should be a panic?
eprintln!("Error: no link between {v} and {w}");
return;
}
self.forest[w].left = None;
Expand Down
29 changes: 17 additions & 12 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
### Benchmark
This benchmark report contains overall running time analysis of Link-Cut trees in comparison to its brute-force counterpart (MBP 14" 16Gb).
This benchmark report contains overall running time analysis of Link-Cut trees in comparison to its brute-force counterpart (iMac 24", M1, 2021, 16Gb).
Each test performs a number of random operations (`link(v, w)`, `cut(v, w)`, `connected(v, w)` or `findmax(v, w)`) on forests of varying sizes.

| # Nodes | # Operations | Random seed | [lctree](https://github.com/azizkayumov/lctree/blob/main/src/lctree.rs) | [brute-force](https://github.com/azizkayumov/lctree/blob/main/tests/test_random.rs) |
| :--- | :--- | :--- | :--- | :--- |
| 100 | 10K | 564315935137013477 | 6.567967ms | 53.48109ms |
| 100 | 100K | 5233351911053448040 | 44.379304ms | 321.900746ms |
| 100 | 1M | 10905789823848117209 | 476.117191ms | 3.915883695s |
| 500 | 2M | 5863263585868731364 | 984.139022ms | 11.542679321s |
| 1000 | 5M | 3900765363016383448 | 2.203334524s | 15.85451642s |
| :--- | :--- | :--- | :--- | :--- |
| 100 | 10K | 14371286973218730379 | 18.005544ms | 291.587072ms |
| 100 | 100K | 18146878621059190265 | 186.174183ms | 3.055154731s |
| 100 | 1M | 6839381432488849859 | 1.824378819s | 30.510083671s |
| 500 | 2M | 12719220817276010307 | 5.17505883s | 303.150073635s |
| 1000 | 5M | 16452801585435658354 | 14.711844242s | 1527.065366409s |

The running time decomposition for the experiment with 1000 nodes and 5M random operations:
The brute force solution takes time for `link(v, w)` and `cut(v, w)` operations where `size(v)` or `size(w)` is the number of points connected to the point.
Then, `connected(v, w)` query can be performed in a constant time.
However, `path(v, w)` operation takes `O(size(v) + size(w))` where `size(v) + size(w) = n` in the worst-case scenario for the brute force.
On the other hand, all of these operations take `O(logn)` amortized time in the case of Link-cut trees.

The time complexity analysis can be observed in practice on the last experiment with 1000 nodes and 5M random operations:

| Operation | Count | [lctree](https://github.com/azizkayumov/lctree/blob/main/src/lctree.rs) | [brute-force](https://github.com/azizkayumov/lctree/blob/main/tests/test_random.rs) |
| :--- | :--- | :--- | :--- |
| link | 1249676 | 481.699835ms | 3.725153109s |
| cut | 1246762 | 809.13079ms | 7.535368601s |
| connected | 1249204 | 432.403952ms | 63.781244ms |
| path | 1249798 | 480.099947ms | 4.530213466s |
| link | 1249509 | 3.671253873s | 1.877281667s |
| cut | 1251768 | 3.694333793s | 4.175882634s |
| connected | 1250180 | 3.662950986s | 79.612576ms |
| path | 1248543 | 3.68330559s | 1520.932589532s |
75 changes: 18 additions & 57 deletions tests/test_random.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
use lctree::LinkCutTree;
use rand::{
rngs::StdRng,
seq::{IteratorRandom, SliceRandom},
Rng, SeedableRng,
};
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
use rand_derive2::RandGen;
use std::{
collections::{HashMap, HashSet},
Expand Down Expand Up @@ -105,15 +101,15 @@ impl BruteForce {
}

if cur == dest {
break;
return max[&dest];
}
for next in &self.adj[cur] {
if !visited.contains(next) {
stack.push((cur, *next));
}
}
}
max[&dest]
usize::MAX
}

pub fn findmin(&self, src: usize, dest: usize) -> usize {
Expand All @@ -135,15 +131,15 @@ impl BruteForce {
}

if cur == dest {
break;
return min[&dest];
}
for next in &self.adj[cur] {
if !visited.contains(next) {
stack.push((cur, *next));
}
}
}
min[&dest]
usize::MAX
}

pub fn findsum(&self, src: usize, dest: usize) -> f64 {
Expand All @@ -165,36 +161,15 @@ impl BruteForce {
}

if cur == dest {
break;
return sum[&dest];
}
for next in &self.adj[cur] {
if !visited.contains(next) {
stack.push((cur, *next));
}
}
}
sum[&dest]
}

pub fn random_edge(&self, rng: &mut StdRng) -> (usize, usize) {
let neighbors = (0..NUMBER_OF_NODES)
.filter(|&v| !self.adj[v].is_empty())
.collect::<Vec<_>>();
if neighbors.is_empty() {
return (usize::MAX, usize::MAX);
}
let v = *neighbors.choose(rng).unwrap();
let w = *self.adj[v].iter().choose(rng).unwrap();
(v, w)
}

pub fn random_connected_pair(&self, rng: &mut StdRng) -> (usize, usize) {
let v = rng.gen_range(0..NUMBER_OF_NODES);
let w = (0..NUMBER_OF_NODES)
.filter(|&w| self.connected(v, w))
.choose(rng)
.unwrap();
(v, w)
f64::MAX
}
}

Expand Down Expand Up @@ -229,52 +204,42 @@ pub fn connectivity() {
let mut brute = BruteForce::new(weights.clone());

// Time the operations:
let operations = ["link", "cut", "connected", "path"];
let mut count_operations = [0; 4];
let mut lctree_time = [Duration::new(0, 0); 4];
let mut brute_time = [Duration::new(0, 0); 4];

// Perform random operations: link, cut, or connected:
for _ in 0..NUMBER_OF_OPERATIONS {
// Choose two random nodes to perform:
// - link: link the two nodes if they are not connected
// - cut: cut the edge between the two nodes if it exists
// - connected: check if the two nodes are connected
// - path: find the maximum weight in the path between the two nodes
let v = rng.gen_range(0..NUMBER_OF_NODES);
let w = rng.gen_range(0..NUMBER_OF_NODES);

// Choose a random operation:
let operation: Operation = rng.gen();
match operation {
Operation::Link => {
// Choose two random nodes to link:
let v = rng.gen_range(0..NUMBER_OF_NODES);
let w = rng.gen_range(0..NUMBER_OF_NODES);

let now = std::time::Instant::now();
lctree.link(v, w);
lctree_time[0] += now.elapsed();

let now = std::time::Instant::now();
brute.link(v, w);
brute_time[0] += now.elapsed();

count_operations[0] += 1;
}
Operation::Cut => {
// Choose a random existing edge to cut:
let (v, w) = brute.random_edge(&mut rng);
if v == w {
continue; // no edges to cut
}

let now = std::time::Instant::now();
lctree.cut(v, w);
lctree_time[1] += now.elapsed();

let now = std::time::Instant::now();
brute.cut(v, w);
brute_time[1] += now.elapsed();

count_operations[1] += 1;
}
Operation::Connected => {
// Choose two random nodes to check if they are connected:
let v = rng.gen_range(0..NUMBER_OF_NODES);
let w = rng.gen_range(0..NUMBER_OF_NODES);

let now = std::time::Instant::now();
let actual = lctree.connected(v, w);
lctree_time[2] += now.elapsed();
Expand All @@ -283,14 +248,9 @@ pub fn connectivity() {
let expected = brute.connected(v, w);
brute_time[2] += now.elapsed();

count_operations[2] += 1;
assert_eq!(actual, expected);
}
Operation::Path => {
// Choose two random nodes from the same tree to find the node
// with the maximum weight in the path between them:
let (v, w) = brute.random_connected_pair(&mut rng);

let now = std::time::Instant::now();
let actual = lctree.path(v, w).max_weight_idx;
lctree_time[3] += now.elapsed();
Expand All @@ -299,10 +259,10 @@ pub fn connectivity() {
let expected = brute.findmax(v, w);
brute_time[3] += now.elapsed();

count_operations[3] += 1;
assert_eq!(actual, expected);
}
}
count_operations[operation as usize] += 1;
}

println!("Number of nodes: {}", NUMBER_OF_NODES);
Expand All @@ -312,6 +272,7 @@ pub fn connectivity() {
"{:10} {:10} {:10} {:10}",
"Operation", "Count", "lctree", "brute"
);
let operations = ["link", "cut", "connected", "path"];
for (i, operation) in operations.iter().enumerate() {
let lctree_op_time = format!("{:?}", lctree_time[i]);
let brute_op_time = format!("{:?}", brute_time[i]);
Expand Down

0 comments on commit 35fc75a

Please sign in to comment.