Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
953
vendor/ruvector/crates/ruvector-mincut/src/expander/mod.rs
vendored
Normal file
953
vendor/ruvector/crates/ruvector-mincut/src/expander/mod.rs
vendored
Normal file
@@ -0,0 +1,953 @@
|
||||
//! Expander Decomposition for Subpolynomial Dynamic Min-Cut
|
||||
//!
|
||||
//! Implements deterministic expander decomposition following:
|
||||
//! - Chuzhoy et al. "Deterministic Algorithms for Decremental Approximate Shortest Paths"
|
||||
//! - Saranurak-Wang "Expander Decomposition and Pruning"
|
||||
//!
|
||||
//! Key property: Decomposes graph into φ-expanders where φ = 1/(log^{O(1)} n)
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! An expander decomposition partitions a graph into components where each component
|
||||
//! has high expansion (conductance). This is critical for achieving subpolynomial
|
||||
//! update time because:
|
||||
//!
|
||||
//! 1. Updates within an expander can be handled efficiently
|
||||
//! 2. The decomposition has O(log n) levels
|
||||
//! 3. Only O(log n / log log n) levels are affected per update
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! The decomposition uses a recursive trimming approach:
|
||||
//!
|
||||
//! 1. **Trimming**: Identify and remove low-conductance components
|
||||
//! 2. **Recursive Partition**: Split remaining graph and recurse
|
||||
//! 3. **Hierarchical Structure**: Build O(log n) level hierarchy
|
||||
//! 4. **Dynamic Updates**: Maintain decomposition under edge insertions/deletions
|
||||
//!
|
||||
//! # Complexity
|
||||
//!
|
||||
//! - Build: O(m log n) where m = edges, n = vertices
|
||||
//! - Conductance computation: O(m)
|
||||
//! - Update: O(log n) levels affected, O(m') work per level
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::sync::Arc;
|
||||
//! use ruvector_mincut::graph::DynamicGraph;
|
||||
//! use ruvector_mincut::expander::ExpanderDecomposition;
|
||||
//!
|
||||
//! // Create a graph
|
||||
//! let graph = Arc::new(DynamicGraph::new());
|
||||
//! graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
//! graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
//! graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
//!
|
||||
//! // Build expander decomposition with conductance threshold
|
||||
//! let phi = 0.1; // Conductance threshold
|
||||
//! let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
//!
|
||||
//! // Query decomposition
|
||||
//! println!("Number of levels: {}", decomp.num_levels());
|
||||
//!
|
||||
//! // Handle dynamic updates
|
||||
//! decomp.insert_edge(1, 4).unwrap();
|
||||
//! decomp.delete_edge(2, 3).unwrap();
|
||||
//! ```
|
||||
|
||||
use crate::error::{MinCutError, Result};
|
||||
use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Expansion (conductance) threshold
|
||||
///
|
||||
/// φ(S) = cut(S, V\S) / min(vol(S), vol(V\S))
|
||||
/// where vol(S) is the sum of degrees of vertices in S
|
||||
pub type Conductance = f64;
|
||||
|
||||
/// A component in the expander decomposition
|
||||
///
|
||||
/// Each component represents a high-expansion subgraph
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExpanderComponent {
|
||||
/// Component ID
|
||||
pub id: usize,
|
||||
/// Vertices in this component
|
||||
pub vertices: HashSet<VertexId>,
|
||||
/// Conductance of this component
|
||||
pub conductance: Conductance,
|
||||
/// Level in the hierarchy (0 = finest, higher = coarser)
|
||||
pub level: usize,
|
||||
/// Inter-component edges (boundary edges)
|
||||
pub boundary_edges: Vec<EdgeId>,
|
||||
/// Volume (sum of degrees) of this component
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
impl ExpanderComponent {
|
||||
/// Create a new expander component
|
||||
fn new(id: usize, vertices: HashSet<VertexId>, level: usize) -> Self {
|
||||
Self {
|
||||
id,
|
||||
vertices,
|
||||
conductance: 0.0,
|
||||
level,
|
||||
boundary_edges: Vec::new(),
|
||||
volume: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this component contains a vertex
|
||||
pub fn contains(&self, v: VertexId) -> bool {
|
||||
self.vertices.contains(&v)
|
||||
}
|
||||
|
||||
/// Get the size of this component
|
||||
pub fn size(&self) -> usize {
|
||||
self.vertices.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Hierarchical expander decomposition
|
||||
///
|
||||
/// Maintains a multi-level decomposition where:
|
||||
/// - Each level contains φ-expander components
|
||||
/// - Lower levels have finer-grained components
|
||||
/// - Higher levels have coarser-grained components
|
||||
pub struct ExpanderDecomposition {
|
||||
/// All components at each level
|
||||
levels: Vec<Vec<ExpanderComponent>>,
|
||||
/// Vertex to component mapping at each level
|
||||
vertex_to_component: Vec<HashMap<VertexId, usize>>,
|
||||
/// Target conductance threshold
|
||||
phi: Conductance,
|
||||
/// Graph reference
|
||||
graph: Arc<DynamicGraph>,
|
||||
/// Next component ID
|
||||
next_component_id: usize,
|
||||
}
|
||||
|
||||
impl ExpanderDecomposition {
|
||||
/// Build expander decomposition with conductance threshold φ
|
||||
///
|
||||
/// Constructs a hierarchical decomposition where each component
|
||||
/// has conductance at least φ (or is maximal)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `graph` - The graph to decompose
|
||||
/// * `phi` - Conductance threshold (typically 1/polylog(n))
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A hierarchical expander decomposition
|
||||
pub fn build(graph: Arc<DynamicGraph>, phi: Conductance) -> Result<Self> {
|
||||
if phi <= 0.0 || phi >= 1.0 {
|
||||
return Err(MinCutError::InvalidParameter(format!(
|
||||
"Conductance phi must be in (0, 1), got {}",
|
||||
phi
|
||||
)));
|
||||
}
|
||||
|
||||
let mut decomp = Self {
|
||||
levels: Vec::new(),
|
||||
vertex_to_component: Vec::new(),
|
||||
phi,
|
||||
graph: graph.clone(),
|
||||
next_component_id: 0,
|
||||
};
|
||||
|
||||
// Build initial decomposition
|
||||
decomp.build_hierarchy()?;
|
||||
|
||||
Ok(decomp)
|
||||
}
|
||||
|
||||
/// Get component containing vertex at given level
|
||||
pub fn component_at_level(&self, v: VertexId, level: usize) -> Option<&ExpanderComponent> {
|
||||
if level >= self.levels.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let component_id = self.vertex_to_component[level].get(&v)?;
|
||||
self.levels[level].iter().find(|c| c.id == *component_id)
|
||||
}
|
||||
|
||||
/// Update after edge insertion
|
||||
///
|
||||
/// Identifies affected components and rebuilds them if necessary
|
||||
pub fn insert_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
|
||||
// Find affected components at each level
|
||||
for level in 0..self.levels.len() {
|
||||
if let (Some(u_comp_id), Some(v_comp_id)) = (
|
||||
self.vertex_to_component[level].get(&u),
|
||||
self.vertex_to_component[level].get(&v),
|
||||
) {
|
||||
// If edge crosses components, update boundary edges
|
||||
if u_comp_id != v_comp_id {
|
||||
// Find the edge ID
|
||||
if let Some(edge) = self.graph.get_edge(u, v) {
|
||||
// Add to boundary edges of both components
|
||||
for comp in &mut self.levels[level] {
|
||||
if comp.id == *u_comp_id || comp.id == *v_comp_id {
|
||||
comp.boundary_edges.push(edge.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Edge within component - may affect conductance
|
||||
let comp_id = *u_comp_id;
|
||||
// Clone vertices to avoid borrow checker issues
|
||||
if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
|
||||
let vertices = comp.vertices.clone();
|
||||
let conductance = self.compute_conductance(&vertices);
|
||||
let volume = self.compute_volume(&vertices);
|
||||
|
||||
// Update component
|
||||
if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id)
|
||||
{
|
||||
comp.conductance = conductance;
|
||||
comp.volume = volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update after edge deletion
|
||||
///
|
||||
/// Identifies affected components and rebuilds them if necessary
|
||||
pub fn delete_edge(&mut self, u: VertexId, _v: VertexId) -> Result<()> {
|
||||
// Find affected components at each level
|
||||
for level in 0..self.levels.len() {
|
||||
if let Some(u_comp_id) = self.vertex_to_component[level].get(&u) {
|
||||
let comp_id = *u_comp_id;
|
||||
|
||||
// Edge deletion may disconnect component - check connectivity
|
||||
// Clone vertices to avoid borrow checker issues
|
||||
if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
|
||||
let vertices = comp.vertices.clone();
|
||||
let is_connected = self.is_connected(&vertices);
|
||||
|
||||
if !is_connected {
|
||||
// Component disconnected - need to split it
|
||||
let _components = self.find_connected_components(&vertices);
|
||||
// This is a simplified approach - full implementation would
|
||||
// rebuild the entire level
|
||||
}
|
||||
|
||||
// Recompute conductance and volume
|
||||
let conductance = self.compute_conductance(&vertices);
|
||||
let volume = self.compute_volume(&vertices);
|
||||
|
||||
// Update component
|
||||
if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id) {
|
||||
comp.conductance = conductance;
|
||||
comp.volume = volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get number of levels in the decomposition
|
||||
pub fn num_levels(&self) -> usize {
|
||||
self.levels.len()
|
||||
}
|
||||
|
||||
/// Build the hierarchical decomposition
|
||||
fn build_hierarchy(&mut self) -> Result<()> {
|
||||
let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
|
||||
|
||||
if all_vertices.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Build single level using deterministic decomposition
|
||||
// Use deterministic decomposition to partition into expanders
|
||||
let components = deterministic_decompose(&self.graph, &all_vertices, self.phi);
|
||||
|
||||
// Create expander components
|
||||
let mut level_components = Vec::new();
|
||||
let mut vertex_map = HashMap::new();
|
||||
|
||||
for vertices in components {
|
||||
let comp_id = self.next_component_id;
|
||||
self.next_component_id += 1;
|
||||
|
||||
let mut component = ExpanderComponent::new(comp_id, vertices.clone(), 0);
|
||||
component.conductance = self.compute_conductance(&vertices);
|
||||
component.volume = self.compute_volume(&vertices);
|
||||
|
||||
// Map vertices to this component
|
||||
for &v in &vertices {
|
||||
vertex_map.insert(v, comp_id);
|
||||
}
|
||||
|
||||
level_components.push(component);
|
||||
}
|
||||
|
||||
self.levels.push(level_components);
|
||||
self.vertex_to_component.push(vertex_map);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute conductance of a vertex set
|
||||
///
|
||||
/// φ(S) = cut(S, V\S) / min(vol(S), vol(V\S))
|
||||
///
|
||||
/// where:
|
||||
/// - cut(S, V\S) = sum of edge weights crossing between S and V\S
|
||||
/// - vol(S) = sum of degrees of vertices in S
|
||||
fn compute_conductance(&self, vertices: &HashSet<VertexId>) -> Conductance {
|
||||
if vertices.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
|
||||
let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
|
||||
|
||||
if complement.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Compute cut value
|
||||
let mut cut_value = 0.0;
|
||||
for &u in vertices {
|
||||
for (v, _) in self.graph.neighbors(u) {
|
||||
if !vertices.contains(&v) {
|
||||
if let Some(weight) = self.graph.edge_weight(u, v) {
|
||||
cut_value += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute volumes
|
||||
let vol_s = self.compute_volume(vertices);
|
||||
let vol_complement = self.compute_volume(&complement);
|
||||
|
||||
if vol_s == 0.0 || vol_complement == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// φ(S) = cut / min(vol(S), vol(V\S))
|
||||
let min_vol = vol_s.min(vol_complement);
|
||||
cut_value / min_vol
|
||||
}
|
||||
|
||||
/// Compute volume (sum of degrees) of a vertex set
|
||||
fn compute_volume(&self, vertices: &HashSet<VertexId>) -> f64 {
|
||||
vertices.iter().map(|&v| self.graph.degree(v) as f64).sum()
|
||||
}
|
||||
|
||||
/// Expander pruning: find low-conductance cut
|
||||
///
|
||||
/// Searches for a subset S with conductance < φ
|
||||
/// Returns None if no such cut exists (component is a φ-expander)
|
||||
fn prune(&self, component: &ExpanderComponent) -> Option<HashSet<VertexId>> {
|
||||
// Try local cut search from each vertex
|
||||
for &start in &component.vertices {
|
||||
if let Some(cut) = self.local_cut_search(start, self.phi, &component.vertices) {
|
||||
let conductance = self.compute_conductance(&cut);
|
||||
if conductance < self.phi {
|
||||
return Some(cut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Deterministic local search for low-conductance cut
|
||||
///
|
||||
/// Performs BFS from start vertex, greedily adding vertices
|
||||
/// to minimize conductance
|
||||
fn local_cut_search(
|
||||
&self,
|
||||
start: VertexId,
|
||||
target_conductance: Conductance,
|
||||
vertices: &HashSet<VertexId>,
|
||||
) -> Option<HashSet<VertexId>> {
|
||||
let mut current_set = HashSet::new();
|
||||
current_set.insert(start);
|
||||
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(start);
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(start);
|
||||
|
||||
let mut best_set = current_set.clone();
|
||||
let mut best_conductance = self.compute_conductance(¤t_set);
|
||||
|
||||
// BFS expansion
|
||||
while let Some(u) = queue.pop_front() {
|
||||
for (v, _) in self.graph.neighbors(u) {
|
||||
// Only consider vertices in the component
|
||||
if !vertices.contains(&v) || visited.contains(&v) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.insert(v);
|
||||
current_set.insert(v);
|
||||
|
||||
let conductance = self.compute_conductance(¤t_set);
|
||||
|
||||
// Keep track of best (lowest conductance) cut found
|
||||
if conductance < best_conductance {
|
||||
best_conductance = conductance;
|
||||
best_set = current_set.clone();
|
||||
}
|
||||
|
||||
// If we found a cut below threshold, return it
|
||||
if conductance < target_conductance {
|
||||
return Some(current_set);
|
||||
}
|
||||
|
||||
queue.push_back(v);
|
||||
|
||||
// Limit search size
|
||||
if current_set.len() >= vertices.len() / 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return best cut found if it's better than starting point
|
||||
if best_conductance < self.phi {
|
||||
Some(best_set)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a set of vertices is connected
|
||||
fn is_connected(&self, vertices: &HashSet<VertexId>) -> bool {
|
||||
if vertices.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let start = *vertices.iter().next().unwrap();
|
||||
let mut visited = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
visited.insert(start);
|
||||
queue.push_back(start);
|
||||
|
||||
while let Some(u) = queue.pop_front() {
|
||||
for (v, _) in self.graph.neighbors(u) {
|
||||
if vertices.contains(&v) && visited.insert(v) {
|
||||
queue.push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visited.len() == vertices.len()
|
||||
}
|
||||
|
||||
/// Find connected components within a vertex set
|
||||
fn find_connected_components(&self, vertices: &HashSet<VertexId>) -> Vec<HashSet<VertexId>> {
|
||||
let mut visited = HashSet::new();
|
||||
let mut components = Vec::new();
|
||||
|
||||
for &start in vertices {
|
||||
if visited.contains(&start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut component = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
component.insert(start);
|
||||
visited.insert(start);
|
||||
queue.push_back(start);
|
||||
|
||||
while let Some(u) = queue.pop_front() {
|
||||
for (v, _) in self.graph.neighbors(u) {
|
||||
if vertices.contains(&v) && visited.insert(v) {
|
||||
component.insert(v);
|
||||
queue.push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
components.push(component);
|
||||
}
|
||||
|
||||
components
|
||||
}
|
||||
}
|
||||
|
||||
/// Spectral gap computation for conductance estimation
|
||||
///
|
||||
/// Estimates the conductance of a component using random walk simulation
|
||||
/// This is a heuristic that approximates the true spectral conductance
|
||||
pub fn estimate_conductance(graph: &DynamicGraph, vertices: &HashSet<VertexId>) -> Conductance {
|
||||
if vertices.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let all_vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
|
||||
let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
|
||||
|
||||
if complement.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Compute cut value
|
||||
let mut cut_value = 0.0;
|
||||
for &u in vertices {
|
||||
for (v, _) in graph.neighbors(u) {
|
||||
if !vertices.contains(&v) {
|
||||
if let Some(weight) = graph.edge_weight(u, v) {
|
||||
cut_value += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute volumes
|
||||
let vol_s: f64 = vertices.iter().map(|&v| graph.degree(v) as f64).sum();
|
||||
let vol_complement: f64 = complement.iter().map(|&v| graph.degree(v) as f64).sum();
|
||||
|
||||
if vol_s == 0.0 || vol_complement == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// φ(S) = cut / min(vol(S), vol(V\S))
|
||||
let min_vol = vol_s.min(vol_complement);
|
||||
cut_value / min_vol
|
||||
}
|
||||
|
||||
/// Deterministic expander decomposition using trimming
|
||||
///
|
||||
/// Recursively partitions the graph into φ-expanders by:
|
||||
/// 1. Finding low-conductance cuts using local search
|
||||
/// 2. Removing them (trimming)
|
||||
/// 3. Recursing on remaining components
|
||||
pub fn deterministic_decompose(
|
||||
graph: &DynamicGraph,
|
||||
vertices: &HashSet<VertexId>,
|
||||
phi: Conductance,
|
||||
) -> Vec<HashSet<VertexId>> {
|
||||
// Base cases
|
||||
if vertices.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if vertices.len() == 1 {
|
||||
return vec![vertices.clone()];
|
||||
}
|
||||
|
||||
// Try to find a low-conductance cut
|
||||
if let Some(cut) = find_low_conductance_cut(graph, vertices, phi) {
|
||||
// Ensure cut is not empty and not the whole set
|
||||
if !cut.is_empty() && cut.len() < vertices.len() {
|
||||
let complement: HashSet<VertexId> = vertices.difference(&cut).copied().collect();
|
||||
|
||||
// Recursively decompose both parts
|
||||
let mut result = deterministic_decompose(graph, &cut, phi);
|
||||
result.extend(deterministic_decompose(graph, &complement, phi));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid low-conductance cut found - this is a φ-expander
|
||||
vec![vertices.clone()]
|
||||
}
|
||||
|
||||
/// Find a low-conductance cut using BFS-based local search
|
||||
fn find_low_conductance_cut(
|
||||
graph: &DynamicGraph,
|
||||
vertices: &HashSet<VertexId>,
|
||||
phi: Conductance,
|
||||
) -> Option<HashSet<VertexId>> {
|
||||
// Try starting from each vertex
|
||||
for &start in vertices {
|
||||
if let Some(cut) = bfs_local_search(graph, start, vertices, phi) {
|
||||
let conductance = estimate_conductance(graph, &cut);
|
||||
if conductance < phi {
|
||||
return Some(cut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// BFS-based local search for low-conductance cuts
|
||||
fn bfs_local_search(
|
||||
graph: &DynamicGraph,
|
||||
start: VertexId,
|
||||
vertices: &HashSet<VertexId>,
|
||||
target_phi: Conductance,
|
||||
) -> Option<HashSet<VertexId>> {
|
||||
let mut current_set = HashSet::new();
|
||||
current_set.insert(start);
|
||||
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(start);
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(start);
|
||||
|
||||
let mut best_set = current_set.clone();
|
||||
let mut best_conductance = estimate_conductance(graph, ¤t_set);
|
||||
|
||||
while let Some(u) = queue.pop_front() {
|
||||
for (v, _) in graph.neighbors(u) {
|
||||
if !vertices.contains(&v) || visited.contains(&v) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.insert(v);
|
||||
current_set.insert(v);
|
||||
|
||||
let conductance = estimate_conductance(graph, ¤t_set);
|
||||
|
||||
if conductance < best_conductance {
|
||||
best_conductance = conductance;
|
||||
best_set = current_set.clone();
|
||||
}
|
||||
|
||||
if conductance < target_phi {
|
||||
return Some(current_set);
|
||||
}
|
||||
|
||||
queue.push_back(v);
|
||||
|
||||
// Limit search size to half the component
|
||||
if current_set.len() >= vertices.len() / 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_conductance < target_phi {
|
||||
Some(best_set)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_graph() -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
// Create a triangle
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
graph
|
||||
}
|
||||
|
||||
fn create_expander_graph() -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
// Create a well-connected graph (complete graph on 5 vertices)
|
||||
for i in 1..=5 {
|
||||
for j in (i + 1)..=5 {
|
||||
graph.insert_edge(i, j, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
fn create_separable_graph() -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
// Two triangles connected by single edge
|
||||
// Triangle 1: 1-2-3
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
// Bridge
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
// Triangle 2: 4-5-6
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 6, 1.0).unwrap();
|
||||
graph.insert_edge(6, 4, 1.0).unwrap();
|
||||
graph
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_simple() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
assert!(decomp.num_levels() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_invalid_phi() {
|
||||
let graph = create_test_graph();
|
||||
|
||||
// Test phi = 0
|
||||
assert!(ExpanderDecomposition::build(graph.clone(), 0.0).is_err());
|
||||
|
||||
// Test phi = 1
|
||||
assert!(ExpanderDecomposition::build(graph.clone(), 1.0).is_err());
|
||||
|
||||
// Test phi > 1
|
||||
assert!(ExpanderDecomposition::build(graph.clone(), 1.5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_conductance_triangle() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
// Single vertex has conductance based on its cut
|
||||
let mut single_vertex = HashSet::new();
|
||||
single_vertex.insert(1);
|
||||
let conductance = decomp.compute_conductance(&single_vertex);
|
||||
|
||||
// Vertex 1 has degree 2, neighbors are 2 and 3
|
||||
// cut({1}, {2,3}) = 2 (both edges from 1)
|
||||
// vol({1}) = 2, vol({2,3}) = 4
|
||||
// φ = 2 / min(2, 4) = 2 / 2 = 1.0
|
||||
assert_eq!(conductance, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_conductance_complete() {
|
||||
let graph = create_expander_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
// Single vertex in complete graph K5
|
||||
let mut single_vertex = HashSet::new();
|
||||
single_vertex.insert(1);
|
||||
let conductance = decomp.compute_conductance(&single_vertex);
|
||||
|
||||
// In K5, each vertex has degree 4
|
||||
// cut({1}, {2,3,4,5}) = 4
|
||||
// vol({1}) = 4, vol({2,3,4,5}) = 16
|
||||
// φ = 4 / min(4, 16) = 4 / 4 = 1.0
|
||||
assert_eq!(conductance, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_volume() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
let mut vertices = HashSet::new();
|
||||
vertices.insert(1);
|
||||
vertices.insert(2);
|
||||
|
||||
// Each vertex in triangle has degree 2
|
||||
// Total volume = 2 + 2 = 4
|
||||
let volume = decomp.compute_volume(&vertices);
|
||||
assert_eq!(volume, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_conductance() {
|
||||
let graph = create_test_graph();
|
||||
|
||||
let mut vertices = HashSet::new();
|
||||
vertices.insert(1);
|
||||
|
||||
let conductance = estimate_conductance(&graph, &vertices);
|
||||
assert_eq!(conductance, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_decompose_triangle() {
|
||||
let graph = create_test_graph();
|
||||
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
|
||||
|
||||
// Low phi will find cuts and decompose
|
||||
// In a triangle, any single vertex has conductance 1.0
|
||||
// (cut of 2 edges, volume of 2: φ = 2/2 = 1.0)
|
||||
let phi = 0.5;
|
||||
let components = deterministic_decompose(&graph, &vertices, phi);
|
||||
|
||||
// With phi=0.5, should split into smaller components
|
||||
// since conductance of single vertices (1.0) > phi (0.5)
|
||||
assert!(components.len() >= 1);
|
||||
assert_eq!(components.iter().map(|c| c.len()).sum::<usize>(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_decompose_separable() {
|
||||
let graph = create_separable_graph();
|
||||
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
|
||||
|
||||
// Low phi should find the bridge cut
|
||||
let phi = 0.3;
|
||||
let components = deterministic_decompose(&graph, &vertices, phi);
|
||||
|
||||
// Should separate into at least 2 components
|
||||
assert!(components.len() >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_at_level() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
// Query component at level 0
|
||||
if decomp.num_levels() > 0 {
|
||||
let comp = decomp.component_at_level(1, 0);
|
||||
assert!(comp.is_some());
|
||||
|
||||
if let Some(c) = comp {
|
||||
assert!(c.contains(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_edge() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
// Add a new edge
|
||||
graph.insert_edge(1, 4, 1.0).unwrap();
|
||||
let result = decomp.insert_edge(1, 4);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_edge() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
// Delete an edge
|
||||
graph.delete_edge(1, 2).unwrap();
|
||||
let result = decomp.delete_edge(1, 2);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_connected() {
|
||||
let graph = create_test_graph();
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
let vertices: HashSet<VertexId> = vec![1, 2, 3].into_iter().collect();
|
||||
assert!(decomp.is_connected(&vertices));
|
||||
|
||||
// Disconnected set
|
||||
let disconnected: HashSet<VertexId> = vec![1, 2].into_iter().collect();
|
||||
// After removing edge 3, vertices 1-2 are still connected
|
||||
assert!(decomp.is_connected(&disconnected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_connected_components() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
// Two separate edges
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
let vertices: HashSet<VertexId> = vec![1, 2, 3, 4].into_iter().collect();
|
||||
let components = decomp.find_connected_components(&vertices);
|
||||
|
||||
assert_eq!(components.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_cut_search() {
|
||||
let graph = create_separable_graph();
|
||||
let phi = 0.3;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
|
||||
|
||||
// Search from vertex 1
|
||||
if let Some(cut) = decomp.local_cut_search(1, phi, &vertices) {
|
||||
assert!(!cut.is_empty());
|
||||
assert!(cut.len() < vertices.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prune() {
|
||||
let graph = create_separable_graph();
|
||||
let phi = 0.3;
|
||||
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
|
||||
|
||||
if decomp.num_levels() > 0 && !decomp.levels[0].is_empty() {
|
||||
let component = &decomp.levels[0][0];
|
||||
|
||||
// Try to prune - may or may not find a cut depending on structure
|
||||
let result = decomp.prune(component);
|
||||
// Just verify it doesn't crash
|
||||
assert!(result.is_some() || result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expander_component_methods() {
|
||||
let mut vertices = HashSet::new();
|
||||
vertices.insert(1);
|
||||
vertices.insert(2);
|
||||
vertices.insert(3);
|
||||
|
||||
let component = ExpanderComponent::new(0, vertices, 0);
|
||||
|
||||
assert_eq!(component.id, 0);
|
||||
assert_eq!(component.level, 0);
|
||||
assert_eq!(component.size(), 3);
|
||||
assert!(component.contains(1));
|
||||
assert!(!component.contains(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
assert_eq!(decomp.num_levels(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_vertex() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.add_vertex(1);
|
||||
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
assert!(decomp.num_levels() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_expander() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a larger complete graph
|
||||
for i in 1..=10 {
|
||||
for j in (i + 1)..=10 {
|
||||
graph.insert_edge(i, j, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let phi = 0.1;
|
||||
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
|
||||
|
||||
assert!(decomp.num_levels() > 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user