8.0 KiB
Witness Trees Implementation
Overview
This document describes the implementation of Witness Trees for dynamic minimum cut maintenance, following the Jin-Sun-Thorup algorithm from SODA 2024: "Fully Dynamic Exact Minimum Cut in Subpolynomial Time".
What are Witness Trees?
Witness trees maintain a spanning forest of a graph where each tree edge is "witnessed" by a cut that certifies its inclusion in the tree. This data structure enables efficient dynamic maintenance of minimum cuts.
Key Properties
- Witness Invariant: Each tree edge (u,v) has a witness cut C such that removing (u,v) from the tree reveals C
- Minimum Cut Certificate: The minimum among all witness cuts equals the graph's minimum cut
- Lazy Updates: Updates are performed lazily to achieve better amortized complexity
Architecture
Core Components
WitnessTree
├── LinkCutTree # Dynamic connectivity queries
├── Witnesses # HashMap of edge witnesses
├── Tree Edges # Spanning forest edges
├── Non-Tree Edges # Cycle-forming edges
└── Min Cut Info # Cached minimum cut value and edges
Key Data Structures
// Witness for a tree edge
pub struct EdgeWitness {
pub tree_edge: (VertexId, VertexId),
pub cut_value: Weight,
pub cut_side: HashSet<VertexId>, // One side of the cut
}
// Main witness tree structure
pub struct WitnessTree {
lct: LinkCutTree, // O(log n) connectivity
witnesses: HashMap<(VertexId, VertexId), EdgeWitness>,
min_cut: Weight,
min_cut_edges: Vec<Edge>,
graph: Arc<RwLock<DynamicGraph>>,
dirty: bool,
tree_edges: HashSet<(VertexId, VertexId)>,
non_tree_edges: HashSet<(VertexId, VertexId)>,
}
Algorithm Details
Build Phase
fn build_spanning_tree() -> Result<()>
-
Spanning Tree Construction (BFS):
- O(n + m) time
- Creates spanning forest for disconnected graphs
- Identifies tree vs non-tree edges
-
Witness Computation:
- For each tree edge (u,v):
- Find components after removing (u,v)
- Compute cut value between components
- Store witness
- For each tree edge (u,v):
Complexity: O(n·m) for initial build
Insert Edge
pub fn insert_edge(u, v, weight) -> Result<Weight>
Case 1: Bridge Edge (u and v in different components)
- Add to spanning tree
- Link in Link-Cut Tree
- Compute witness for new edge
- Update min cut if needed
Case 2: Cycle Edge (u and v already connected)
- Add to non-tree edges
- Mark dirty for recomputation
- May improve minimum cut
Complexity: Amortized O(log n) with lazy updates
Delete Edge
pub fn delete_edge(u, v) -> Result<Weight>
Case 1: Tree Edge
- Remove from spanning tree
- Cut in Link-Cut Tree
- Find replacement edge in non-tree edges
- If found: add to tree, compute witness
- Update min cut
Case 2: Non-Tree Edge
- Remove from non-tree edges
- Mark dirty for recomputation
Complexity: O(m) worst case (finding replacement), O(log n) amortized
Finding Minimum Cut
fn recompute_min_cut()
- Examine all tree edge witnesses
- Find witness with minimum cut value
- Collect edges in that cut
- Cache result
Complexity: O(number of tree edges) = O(n)
Optimizations
1. Lazy Witness Tree
pub struct LazyWitnessTree {
inner: WitnessTree,
pending_updates: Vec<(VertexId, VertexId, bool)>,
batch_threshold: usize,
}
- Batches updates together
- Flushes when threshold reached
- Better amortized complexity for sequences
2. Link-Cut Tree Integration
- O(log n) connectivity queries
- O(log n) link/cut operations
- Path compression for efficiency
3. Canonical Edge Keys
fn canonical_key(u, v) -> (VertexId, VertexId) {
if u <= v { (u, v) } else { (v, u) }
}
- Consistent edge representation
- Efficient HashMap lookups
- Avoids duplicate edges
Complexity Analysis
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Build | O(n·m) | O(n + m) |
| Insert Edge | O(log n) amortized | O(1) |
| Delete Edge | O(m) worst, O(log n) amortized | O(1) |
| Min Cut Query | O(1) | - |
| Find Witness | O(1) | - |
Implementation Notes
Thread Safety
The implementation uses Arc<RwLock<DynamicGraph>> for thread-safe graph access:
- Multiple concurrent reads allowed
- Exclusive write access when modifying
Edge Cases Handled
- Empty Graph: Returns ∞ for min cut
- Disconnected Graph: Returns 0 (no cut exists)
- Single Vertex: Returns ∞
- Dynamic Vertices: Automatically adds new vertices to LCT
Limitations
- Spanning Tree Dependency: Only considers cuts corresponding to tree edges
- Approximation: May not find optimal cut if it doesn't correspond to tree structure
- Replacement Search: Finding replacement edges is O(m) in worst case
Testing
The implementation includes 20 comprehensive tests:
Basic Functionality
test_build_empty- Empty graph handlingtest_build_single_vertex- Single vertextest_build_triangle- Simple connected graphtest_build_bridge- Bridge detection
Dynamic Updates
test_insert_bridge_edge- Adding bridge edgestest_insert_cycle_edge- Adding cycle edgestest_delete_tree_edge- Removing tree edgestest_delete_non_tree_edge- Removing non-tree edgestest_dynamic_sequence- Sequence of operations
Correctness
test_is_tree_edge- Tree edge identificationtest_find_witness- Witness retrievaltest_tree_edge_cut- Cut value computationtest_weighted_edges- Weighted graph supporttest_canonical_key- Edge key normalization
Advanced Features
test_lazy_witness_tree- Lazy updatestest_lazy_witness_batch_threshold- Batchingtest_disconnected_graph- Multiple componentstest_large_graph- Scalability (100 vertices)test_complete_graph- Dense graphs
All Tests Pass ✓
test result: ok. 20 passed; 0 failed; 0 ignored
Usage Examples
Basic Usage
use std::sync::Arc;
use parking_lot::RwLock;
use ruvector_mincut::{DynamicGraph, WitnessTree};
// Create graph
let graph = Arc::new(RwLock::new(DynamicGraph::new()));
graph.write().insert_edge(1, 2, 1.0).unwrap();
graph.write().insert_edge(2, 3, 1.0).unwrap();
graph.write().insert_edge(3, 1, 1.0).unwrap();
// Build witness tree
let mut witness = WitnessTree::build(graph.clone()).unwrap();
// Query minimum cut
println!("Min cut: {}", witness.min_cut_value());
println!("Cut edges: {:?}", witness.min_cut_edges());
Dynamic Updates
// Insert edge
graph.write().insert_edge(1, 4, 2.0).unwrap();
let new_cut = witness.insert_edge(1, 4, 2.0).unwrap();
println!("New min cut: {}", new_cut);
// Delete edge
graph.write().delete_edge(1, 2).unwrap();
let updated_cut = witness.delete_edge(1, 2).unwrap();
Lazy Updates
use ruvector_mincut::LazyWitnessTree;
let mut lazy = LazyWitnessTree::with_threshold(graph, 10).unwrap();
// Batch updates
for i in 1..10 {
graph.write().insert_edge(i, i+1, 1.0).unwrap();
lazy.insert_edge(i, i+1, 1.0).unwrap();
}
// Force flush and get result
let min_cut = lazy.min_cut_value();
Future Improvements
- Parallel Witness Computation: Compute witnesses in parallel for large graphs
- Incremental Updates: More efficient incremental witness updates
- Approximate Witnesses: Trade accuracy for speed in large graphs
- Persistent Data Structures: Better support for versioning and rollback
References
- Jin, C., & Sun, R., & Thorup, M. (2024). "Fully Dynamic Exact Minimum Cut in Subpolynomial Time". SODA 2024.
- Sleator, D. D., & Tarjan, R. E. (1983). "A data structure for dynamic trees". Journal of Computer and System Sciences.
File Location
/home/user/ruvector/crates/ruvector-mincut/src/witness/mod.rs
Integration
The witness tree module is fully integrated into the ruvector-mincut crate:
pub use witness::{WitnessTree, LazyWitnessTree, EdgeWitness};
Available in the prelude for convenient access.