From 35fc75a3d5107df0fbc2e31ca6bf1d5df5b4386b Mon Sep 17 00:00:00 2001 From: Aziz Kayumov Date: Thu, 2 Nov 2023 14:14:07 +0900 Subject: [PATCH] closes #36 --- src/lctree.rs | 5 +-- tests/README.md | 29 ++++++++++------- tests/test_random.rs | 75 +++++++++++--------------------------------- 3 files changed, 38 insertions(+), 71 deletions(-) diff --git a/src/lctree.rs b/src/lctree.rs index d5b2e4c..75d5401 100644 --- a/src/lctree.rs +++ b/src/lctree.rs @@ -77,8 +77,9 @@ impl LinkCutTree { } // 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; diff --git a/tests/README.md b/tests/README.md index d4dbb36..8540190 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 | diff --git a/tests/test_random.rs b/tests/test_random.rs index c6c9cf3..5b4adca 100644 --- a/tests/test_random.rs +++ b/tests/test_random.rs @@ -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}, @@ -105,7 +101,7 @@ impl BruteForce { } if cur == dest { - break; + return max[&dest]; } for next in &self.adj[cur] { if !visited.contains(next) { @@ -113,7 +109,7 @@ impl BruteForce { } } } - max[&dest] + usize::MAX } pub fn findmin(&self, src: usize, dest: usize) -> usize { @@ -135,7 +131,7 @@ impl BruteForce { } if cur == dest { - break; + return min[&dest]; } for next in &self.adj[cur] { if !visited.contains(next) { @@ -143,7 +139,7 @@ impl BruteForce { } } } - min[&dest] + usize::MAX } pub fn findsum(&self, src: usize, dest: usize) -> f64 { @@ -165,7 +161,7 @@ impl BruteForce { } if cur == dest { - break; + return sum[&dest]; } for next in &self.adj[cur] { if !visited.contains(next) { @@ -173,28 +169,7 @@ impl BruteForce { } } } - 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::>(); - 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 } } @@ -229,20 +204,24 @@ 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(); @@ -250,16 +229,8 @@ pub fn connectivity() { 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(); @@ -267,14 +238,8 @@ pub fn connectivity() { 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(); @@ -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(); @@ -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); @@ -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]);