git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
1385 lines
42 KiB
Rust
1385 lines
42 KiB
Rust
//! Subpolynomial Dynamic Minimum Cut Algorithm
|
|
//!
|
|
//! Implementation of the December 2024 breakthrough achieving n^{o(1)} update time:
|
|
//! "Deterministic and Exact Fully-dynamic Minimum Cut of Superpolylogarithmic Size
|
|
//! in Subpolynomial Time" (arXiv:2512.13105)
|
|
//!
|
|
//! # Key Components
|
|
//!
|
|
//! 1. **Multi-Level Hierarchy**: O(log^{1/4} n) levels of expander decomposition
|
|
//! 2. **Deterministic LocalKCut**: Tree packing with color-coded enumeration
|
|
//! 3. **Fragmenting Algorithm**: Boundary-sparse cut detection
|
|
//! 4. **Witness Trees**: Certificate-based cut verification
|
|
//!
|
|
//! # Complexity Guarantees
|
|
//!
|
|
//! - **Update Time**: O(n^{o(1)}) = 2^{O(log^{1-c} n)} amortized
|
|
//! - **Query Time**: O(1)
|
|
//! - **Space**: O(m log n)
|
|
//! - **Cut Size**: Up to 2^{Θ(log^{3/4-c} n)}
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use ruvector_mincut::subpolynomial::{SubpolynomialMinCut, SubpolyConfig};
|
|
//!
|
|
//! let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
//!
|
|
//! // Build initial graph
|
|
//! mincut.insert_edge(1, 2, 1.0);
|
|
//! mincut.insert_edge(2, 3, 1.0);
|
|
//! mincut.insert_edge(3, 1, 1.0);
|
|
//!
|
|
//! // Query minimum cut
|
|
//! let cut_value = mincut.min_cut_value();
|
|
//! println!("Min cut: {}", cut_value);
|
|
//!
|
|
//! // Updates are subpolynomial!
|
|
//! mincut.insert_edge(3, 4, 1.0);
|
|
//! println!("New min cut: {}", mincut.min_cut_value());
|
|
//! ```
|
|
|
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
use std::time::Instant;
|
|
|
|
use crate::cluster::hierarchy::{
|
|
Expander, HierarchyCluster, HierarchyConfig, Precluster, ThreeLevelHierarchy,
|
|
};
|
|
use crate::error::{MinCutError, Result};
|
|
use crate::expander::{ExpanderComponent, ExpanderDecomposition};
|
|
use crate::fragmentation::{Fragmentation, FragmentationConfig, TrimResult};
|
|
use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
|
|
use crate::localkcut::deterministic::{DeterministicLocalKCut, LocalCut as DetLocalCut};
|
|
use crate::witness::{LazyWitnessTree, WitnessTree};
|
|
|
|
/// Configuration for the subpolynomial algorithm
|
|
#[derive(Debug, Clone)]
|
|
pub struct SubpolyConfig {
|
|
/// Expansion parameter φ = 2^{-Θ(log^{3/4} n)}
|
|
/// For n < 10^6, we use a practical approximation
|
|
pub phi: f64,
|
|
/// Maximum cut size to support exactly
|
|
/// λ_max = 2^{Θ(log^{3/4-c} n)}
|
|
pub lambda_max: u64,
|
|
/// Approximation parameter ε for (1+ε)-approximate internal operations
|
|
pub epsilon: f64,
|
|
/// Target number of hierarchy levels: O(log^{1/4} n)
|
|
pub target_levels: usize,
|
|
/// Enable recourse tracking for complexity verification
|
|
pub track_recourse: bool,
|
|
/// Enable mirror cut certification
|
|
pub certify_cuts: bool,
|
|
/// Enable parallel processing
|
|
pub parallel: bool,
|
|
}
|
|
|
|
impl Default for SubpolyConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
phi: 0.01,
|
|
lambda_max: 1000,
|
|
epsilon: 0.1,
|
|
target_levels: 4, // O(log^{1/4} n) for n ~= 10^6
|
|
track_recourse: true,
|
|
certify_cuts: true,
|
|
parallel: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SubpolyConfig {
|
|
/// Create config optimized for graph of size n
|
|
pub fn for_size(n: usize) -> Self {
|
|
let log_n = (n.max(2) as f64).ln();
|
|
|
|
// φ = 2^{-Θ(log^{3/4} n)}
|
|
let phi = 2.0_f64.powf(-log_n.powf(0.75) / 4.0);
|
|
|
|
// λ_max = 2^{Θ(log^{3/4-c} n)} with c = 0.1
|
|
let lambda_max = 2.0_f64.powf(log_n.powf(0.65)).min(1e9) as u64;
|
|
|
|
// Target levels = O(log^{1/4} n)
|
|
let target_levels = (log_n.powf(0.25).ceil() as usize).max(2).min(10);
|
|
|
|
Self {
|
|
phi,
|
|
lambda_max,
|
|
epsilon: 0.1,
|
|
target_levels,
|
|
track_recourse: true,
|
|
certify_cuts: true,
|
|
parallel: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Statistics for recourse tracking
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RecourseStats {
|
|
/// Total recourse across all updates
|
|
pub total_recourse: u64,
|
|
/// Number of updates processed
|
|
pub num_updates: u64,
|
|
/// Maximum recourse in a single update
|
|
pub max_single_recourse: u64,
|
|
/// Recourse per level
|
|
pub recourse_per_level: Vec<u64>,
|
|
/// Average update time in microseconds
|
|
pub avg_update_time_us: f64,
|
|
/// Theoretical subpolynomial bound (computed)
|
|
pub theoretical_bound: f64,
|
|
}
|
|
|
|
impl RecourseStats {
|
|
/// Check if recourse is within subpolynomial bounds
|
|
pub fn is_subpolynomial(&self, n: usize) -> bool {
|
|
if n < 2 || self.num_updates == 0 {
|
|
return true;
|
|
}
|
|
|
|
let log_n = (n as f64).ln();
|
|
// Subpolynomial: 2^{O(log^{1-c} n)} with c = 0.1
|
|
let bound = 2.0_f64.powf(log_n.powf(0.9));
|
|
|
|
(self.total_recourse as f64 / self.num_updates as f64) <= bound
|
|
}
|
|
|
|
/// Get amortized recourse per update
|
|
pub fn amortized_recourse(&self) -> f64 {
|
|
if self.num_updates == 0 {
|
|
return 0.0;
|
|
}
|
|
self.total_recourse as f64 / self.num_updates as f64
|
|
}
|
|
}
|
|
|
|
/// A level in the multi-level hierarchy
|
|
#[derive(Debug)]
|
|
pub struct HierarchyLevel {
|
|
/// Level index (0 = base, higher = coarser)
|
|
pub level: usize,
|
|
/// Expander decomposition at this level
|
|
pub expanders: HashMap<u64, LevelExpander>,
|
|
/// Vertex to expander mapping
|
|
pub vertex_expander: HashMap<VertexId, u64>,
|
|
/// Next expander ID
|
|
next_id: u64,
|
|
/// Recourse at this level
|
|
pub recourse: u64,
|
|
/// Configuration
|
|
phi: f64,
|
|
}
|
|
|
|
/// An expander within a hierarchy level
|
|
#[derive(Debug, Clone)]
|
|
pub struct LevelExpander {
|
|
/// Unique ID
|
|
pub id: u64,
|
|
/// Vertices in this expander
|
|
pub vertices: HashSet<VertexId>,
|
|
/// Boundary edges
|
|
pub boundary_size: usize,
|
|
/// Volume (sum of degrees)
|
|
pub volume: usize,
|
|
/// Certified minimum cut within expander
|
|
pub internal_min_cut: f64,
|
|
/// Is this a valid φ-expander?
|
|
pub is_valid_expander: bool,
|
|
/// Parent expander ID at next level (if any)
|
|
pub parent_id: Option<u64>,
|
|
/// Child expander IDs at previous level
|
|
pub children_ids: Vec<u64>,
|
|
}
|
|
|
|
/// The main subpolynomial dynamic minimum cut structure
|
|
#[derive(Debug)]
|
|
pub struct SubpolynomialMinCut {
|
|
/// Configuration
|
|
config: SubpolyConfig,
|
|
/// Graph adjacency
|
|
adjacency: HashMap<VertexId, HashMap<VertexId, Weight>>,
|
|
/// All edges
|
|
edges: HashSet<(VertexId, VertexId)>,
|
|
/// Multi-level hierarchy
|
|
levels: Vec<HierarchyLevel>,
|
|
/// Deterministic LocalKCut for cut discovery
|
|
local_kcut: Option<DeterministicLocalKCut>,
|
|
/// Current minimum cut value
|
|
current_min_cut: f64,
|
|
/// Recourse statistics
|
|
recourse_stats: RecourseStats,
|
|
/// Number of vertices
|
|
num_vertices: usize,
|
|
/// Number of edges
|
|
num_edges: usize,
|
|
/// Next vertex/edge ID tracking
|
|
next_id: u64,
|
|
/// Is hierarchy built?
|
|
hierarchy_built: bool,
|
|
}
|
|
|
|
impl SubpolynomialMinCut {
|
|
/// Create new subpolynomial min-cut structure
|
|
pub fn new(config: SubpolyConfig) -> Self {
|
|
let num_levels = config.target_levels;
|
|
let levels = (0..num_levels)
|
|
.map(|i| HierarchyLevel {
|
|
level: i,
|
|
expanders: HashMap::new(),
|
|
vertex_expander: HashMap::new(),
|
|
next_id: 1,
|
|
recourse: 0,
|
|
phi: config.phi * (1.0 + i as f64 * 0.1), // Slightly increasing φ per level
|
|
})
|
|
.collect();
|
|
|
|
Self {
|
|
config,
|
|
adjacency: HashMap::new(),
|
|
edges: HashSet::new(),
|
|
levels,
|
|
local_kcut: None,
|
|
current_min_cut: f64::INFINITY,
|
|
recourse_stats: RecourseStats::default(),
|
|
num_vertices: 0,
|
|
num_edges: 0,
|
|
next_id: 1,
|
|
hierarchy_built: false,
|
|
}
|
|
}
|
|
|
|
/// Create with config optimized for expected graph size
|
|
pub fn for_size(expected_n: usize) -> Self {
|
|
Self::new(SubpolyConfig::for_size(expected_n))
|
|
}
|
|
|
|
/// Insert an edge
|
|
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<f64> {
|
|
let start = Instant::now();
|
|
|
|
let key = Self::edge_key(u, v);
|
|
if self.edges.contains(&key) {
|
|
return Err(MinCutError::EdgeExists(u, v));
|
|
}
|
|
|
|
// Add to graph
|
|
self.edges.insert(key);
|
|
let is_new_u = !self.adjacency.contains_key(&u);
|
|
let is_new_v = !self.adjacency.contains_key(&v);
|
|
|
|
self.adjacency.entry(u).or_default().insert(v, weight);
|
|
self.adjacency.entry(v).or_default().insert(u, weight);
|
|
|
|
if is_new_u {
|
|
self.num_vertices += 1;
|
|
}
|
|
if is_new_v && u != v {
|
|
self.num_vertices += 1;
|
|
}
|
|
self.num_edges += 1;
|
|
|
|
// Update hierarchy incrementally if built
|
|
if self.hierarchy_built {
|
|
let recourse = self.handle_edge_insert(u, v, weight);
|
|
self.update_recourse_stats(recourse, start.elapsed().as_micros() as f64);
|
|
}
|
|
|
|
// Update LocalKCut
|
|
if let Some(ref mut lkc) = self.local_kcut {
|
|
lkc.insert_edge(u, v, weight);
|
|
}
|
|
|
|
Ok(self.current_min_cut)
|
|
}
|
|
|
|
/// Delete an edge
|
|
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<f64> {
|
|
let start = Instant::now();
|
|
|
|
let key = Self::edge_key(u, v);
|
|
if !self.edges.remove(&key) {
|
|
return Err(MinCutError::EdgeNotFound(u, v));
|
|
}
|
|
|
|
// Remove from graph
|
|
if let Some(neighbors) = self.adjacency.get_mut(&u) {
|
|
neighbors.remove(&v);
|
|
}
|
|
if let Some(neighbors) = self.adjacency.get_mut(&v) {
|
|
neighbors.remove(&u);
|
|
}
|
|
self.num_edges -= 1;
|
|
|
|
// Update hierarchy incrementally if built
|
|
if self.hierarchy_built {
|
|
let recourse = self.handle_edge_delete(u, v);
|
|
self.update_recourse_stats(recourse, start.elapsed().as_micros() as f64);
|
|
}
|
|
|
|
// Update LocalKCut
|
|
if let Some(ref mut lkc) = self.local_kcut {
|
|
lkc.delete_edge(u, v);
|
|
}
|
|
|
|
Ok(self.current_min_cut)
|
|
}
|
|
|
|
/// Build the multi-level hierarchy
|
|
///
|
|
/// This creates O(log^{1/4} n) levels of expander decomposition,
|
|
/// enabling subpolynomial update time.
|
|
pub fn build(&mut self) {
|
|
if self.adjacency.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Adjust number of levels based on actual graph size
|
|
let n = self.num_vertices;
|
|
let log_n = (n.max(2) as f64).ln();
|
|
let optimal_levels = (log_n.powf(0.25).ceil() as usize).max(2).min(10);
|
|
|
|
// Resize levels if needed
|
|
while self.levels.len() < optimal_levels {
|
|
let i = self.levels.len();
|
|
self.levels.push(HierarchyLevel {
|
|
level: i,
|
|
expanders: HashMap::new(),
|
|
vertex_expander: HashMap::new(),
|
|
next_id: 1,
|
|
recourse: 0,
|
|
phi: self.config.phi * (1.0 + i as f64 * 0.1),
|
|
});
|
|
}
|
|
|
|
// Build base level (level 0) expanders
|
|
self.build_base_level();
|
|
|
|
// Build subsequent levels by coarsening
|
|
for level in 1..self.levels.len() {
|
|
self.build_level(level);
|
|
}
|
|
|
|
// Initialize LocalKCut
|
|
self.local_kcut = Some(DeterministicLocalKCut::new(
|
|
self.config.lambda_max,
|
|
self.num_vertices * 10,
|
|
2,
|
|
));
|
|
|
|
// Collect edge data first
|
|
let edge_data: Vec<(VertexId, VertexId, Weight)> = self
|
|
.edges
|
|
.iter()
|
|
.map(|&(u, v)| (u, v, self.get_weight(u, v).unwrap_or(1.0)))
|
|
.collect();
|
|
|
|
// Add edges to LocalKCut
|
|
if let Some(ref mut lkc) = self.local_kcut {
|
|
for (u, v, weight) in edge_data {
|
|
lkc.insert_edge(u, v, weight);
|
|
}
|
|
}
|
|
|
|
// Compute initial minimum cut
|
|
self.recompute_min_cut();
|
|
|
|
self.hierarchy_built = true;
|
|
|
|
// Update theoretical bound
|
|
self.recourse_stats.theoretical_bound = 2.0_f64.powf(log_n.powf(0.9));
|
|
}
|
|
|
|
/// Build the base level (level 0) expanders
|
|
fn build_base_level(&mut self) {
|
|
let vertices: HashSet<_> = self.adjacency.keys().copied().collect();
|
|
if vertices.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Get phi from level before mutating
|
|
let phi = self.levels[0].phi;
|
|
|
|
// First pass: grow all expanders (uses immutable self)
|
|
let mut remaining = vertices.clone();
|
|
let mut expander_sets: Vec<HashSet<VertexId>> = Vec::new();
|
|
|
|
while !remaining.is_empty() {
|
|
let start = *remaining.iter().next().unwrap();
|
|
let expander_vertices = self.grow_expander(&remaining, start, phi);
|
|
|
|
if expander_vertices.is_empty() {
|
|
remaining.remove(&start);
|
|
continue;
|
|
}
|
|
|
|
for &v in &expander_vertices {
|
|
remaining.remove(&v);
|
|
}
|
|
|
|
expander_sets.push(expander_vertices);
|
|
}
|
|
|
|
// Second pass: compute properties (uses immutable self)
|
|
let mut expanders_to_create: Vec<LevelExpander> = Vec::new();
|
|
let mut vertex_mappings: Vec<(VertexId, u64)> = Vec::new();
|
|
let mut next_id = self.levels[0].next_id;
|
|
|
|
for expander_vertices in &expander_sets {
|
|
let id = next_id;
|
|
next_id += 1;
|
|
|
|
let volume = expander_vertices.iter().map(|&v| self.degree(v)).sum();
|
|
|
|
let boundary_size = self.count_boundary(expander_vertices);
|
|
|
|
let expander = LevelExpander {
|
|
id,
|
|
vertices: expander_vertices.clone(),
|
|
boundary_size,
|
|
volume,
|
|
internal_min_cut: f64::INFINITY,
|
|
is_valid_expander: true,
|
|
parent_id: None,
|
|
children_ids: Vec::new(),
|
|
};
|
|
|
|
for &v in expander_vertices {
|
|
vertex_mappings.push((v, id));
|
|
}
|
|
|
|
expanders_to_create.push(expander);
|
|
}
|
|
|
|
// Third pass: apply all changes (uses mutable self)
|
|
let level = &mut self.levels[0];
|
|
level.expanders.clear();
|
|
level.vertex_expander.clear();
|
|
level.next_id = next_id;
|
|
|
|
for expander in expanders_to_create {
|
|
level.expanders.insert(expander.id, expander);
|
|
}
|
|
|
|
for (v, id) in vertex_mappings {
|
|
level.vertex_expander.insert(v, id);
|
|
}
|
|
}
|
|
|
|
/// Build a level by coarsening the previous level
|
|
fn build_level(&mut self, level_idx: usize) {
|
|
if level_idx == 0 || level_idx >= self.levels.len() {
|
|
return;
|
|
}
|
|
|
|
// Collect expander IDs from previous level
|
|
let prev_expander_ids: Vec<u64> = self.levels[level_idx - 1]
|
|
.expanders
|
|
.keys()
|
|
.copied()
|
|
.collect();
|
|
|
|
// Group adjacent expanders
|
|
let mut groups: Vec<Vec<u64>> = Vec::new();
|
|
let mut assigned: HashSet<u64> = HashSet::new();
|
|
|
|
for &exp_id in &prev_expander_ids {
|
|
if assigned.contains(&exp_id) {
|
|
continue;
|
|
}
|
|
|
|
let mut group = vec![exp_id];
|
|
assigned.insert(exp_id);
|
|
|
|
// Find adjacent expanders
|
|
for &other_id in &prev_expander_ids {
|
|
if assigned.contains(&other_id) {
|
|
continue;
|
|
}
|
|
|
|
if self.expanders_adjacent_at_level(level_idx - 1, exp_id, other_id) {
|
|
group.push(other_id);
|
|
assigned.insert(other_id);
|
|
|
|
// Limit group size
|
|
if group.len() >= 4 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
groups.push(group);
|
|
}
|
|
|
|
// First, collect all vertices from child expanders
|
|
let mut group_vertices: Vec<(Vec<u64>, HashSet<VertexId>)> = Vec::new();
|
|
for group in &groups {
|
|
let mut vertices = HashSet::new();
|
|
for &child_id in group {
|
|
if let Some(child) = self.levels[level_idx - 1].expanders.get(&child_id) {
|
|
vertices.extend(&child.vertices);
|
|
}
|
|
}
|
|
group_vertices.push((group.clone(), vertices));
|
|
}
|
|
|
|
// Now compute properties using immutable self
|
|
let mut expanders_to_create: Vec<(u64, LevelExpander, HashMap<VertexId, u64>)> = Vec::new();
|
|
let mut next_id = self.levels[level_idx].next_id;
|
|
|
|
for (group, vertices) in &group_vertices {
|
|
let id = next_id;
|
|
next_id += 1;
|
|
|
|
let volume = vertices.iter().map(|&v| self.degree(v)).sum();
|
|
|
|
let boundary_size = self.count_boundary(vertices);
|
|
|
|
let expander = LevelExpander {
|
|
id,
|
|
vertices: vertices.clone(),
|
|
boundary_size,
|
|
volume,
|
|
internal_min_cut: f64::INFINITY,
|
|
is_valid_expander: true,
|
|
parent_id: None,
|
|
children_ids: group.clone(),
|
|
};
|
|
|
|
let mut vertex_map = HashMap::new();
|
|
for &v in vertices {
|
|
vertex_map.insert(v, id);
|
|
}
|
|
|
|
expanders_to_create.push((id, expander, vertex_map));
|
|
}
|
|
|
|
// Now apply all changes
|
|
let level = &mut self.levels[level_idx];
|
|
level.expanders.clear();
|
|
level.vertex_expander.clear();
|
|
level.next_id = next_id;
|
|
|
|
for (id, expander, vertex_map) in expanders_to_create {
|
|
level.expanders.insert(id, expander);
|
|
level.vertex_expander.extend(vertex_map);
|
|
}
|
|
|
|
// Update parent pointers in children (separate borrow)
|
|
for (group, _) in &group_vertices {
|
|
// Find the parent ID for this group
|
|
let parent_id = self.levels[level_idx]
|
|
.expanders
|
|
.values()
|
|
.find(|e| &e.children_ids == group)
|
|
.map(|e| e.id);
|
|
|
|
if let Some(pid) = parent_id {
|
|
for &child_id in group {
|
|
if let Some(child) = self.levels[level_idx - 1].expanders.get_mut(&child_id) {
|
|
child.parent_id = Some(pid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Grow an expander from a starting vertex
|
|
fn grow_expander(
|
|
&self,
|
|
available: &HashSet<VertexId>,
|
|
start: VertexId,
|
|
phi: f64,
|
|
) -> HashSet<VertexId> {
|
|
let mut expander = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
|
|
queue.push_back(start);
|
|
expander.insert(start);
|
|
|
|
let max_size = (self.num_vertices / 4).max(10);
|
|
|
|
while let Some(v) = queue.pop_front() {
|
|
if expander.len() >= max_size {
|
|
break;
|
|
}
|
|
|
|
for (neighbor, _) in self.neighbors(v) {
|
|
if !available.contains(&neighbor) || expander.contains(&neighbor) {
|
|
continue;
|
|
}
|
|
|
|
// Check expansion property
|
|
let mut test_set = expander.clone();
|
|
test_set.insert(neighbor);
|
|
|
|
let volume: usize = test_set.iter().map(|&u| self.degree(u)).sum();
|
|
let boundary = self.count_boundary(&test_set);
|
|
|
|
let expansion = if volume > 0 {
|
|
boundary as f64 / volume as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Add if it maintains reasonable expansion
|
|
if expansion >= phi * 0.5 || expander.len() < 3 {
|
|
expander.insert(neighbor);
|
|
queue.push_back(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
expander
|
|
}
|
|
|
|
/// Handle edge insertion with subpolynomial update
|
|
fn handle_edge_insert(&mut self, u: VertexId, v: VertexId, weight: Weight) -> u64 {
|
|
let mut total_recourse = 0u64;
|
|
|
|
// Find affected expanders at each level
|
|
for level_idx in 0..self.levels.len() {
|
|
let recourse = self.update_level_for_insert(level_idx, u, v, weight);
|
|
total_recourse += recourse;
|
|
|
|
if level_idx < self.levels.len() {
|
|
self.levels[level_idx].recourse += recourse;
|
|
}
|
|
}
|
|
|
|
// Update minimum cut
|
|
self.update_min_cut_incremental(u, v, true);
|
|
|
|
total_recourse
|
|
}
|
|
|
|
/// Handle edge deletion with subpolynomial update
|
|
fn handle_edge_delete(&mut self, u: VertexId, v: VertexId) -> u64 {
|
|
let mut total_recourse = 0u64;
|
|
|
|
// Find affected expanders at each level
|
|
for level_idx in 0..self.levels.len() {
|
|
let recourse = self.update_level_for_delete(level_idx, u, v);
|
|
total_recourse += recourse;
|
|
|
|
if level_idx < self.levels.len() {
|
|
self.levels[level_idx].recourse += recourse;
|
|
}
|
|
}
|
|
|
|
// Update minimum cut
|
|
self.update_min_cut_incremental(u, v, false);
|
|
|
|
total_recourse
|
|
}
|
|
|
|
/// Update a level for edge insertion
|
|
fn update_level_for_insert(
|
|
&mut self,
|
|
level_idx: usize,
|
|
u: VertexId,
|
|
v: VertexId,
|
|
_weight: Weight,
|
|
) -> u64 {
|
|
if level_idx >= self.levels.len() {
|
|
return 0;
|
|
}
|
|
|
|
let mut recourse = 0u64;
|
|
|
|
// Get expanders containing u and v
|
|
let exp_u = self.levels[level_idx].vertex_expander.get(&u).copied();
|
|
let exp_v = self.levels[level_idx].vertex_expander.get(&v).copied();
|
|
|
|
match (exp_u, exp_v) {
|
|
(Some(eu), Some(ev)) if eu == ev => {
|
|
// Same expander - just update internal properties
|
|
recourse += 1;
|
|
self.update_expander_properties(level_idx, eu);
|
|
}
|
|
(Some(eu), Some(ev)) => {
|
|
// Different expanders - update both
|
|
recourse += 2;
|
|
self.update_expander_properties(level_idx, eu);
|
|
self.update_expander_properties(level_idx, ev);
|
|
}
|
|
(Some(eu), None) => {
|
|
// Add v to expander containing u
|
|
recourse += self.try_add_to_expander(level_idx, v, eu);
|
|
}
|
|
(None, Some(ev)) => {
|
|
// Add u to expander containing v
|
|
recourse += self.try_add_to_expander(level_idx, u, ev);
|
|
}
|
|
(None, None) => {
|
|
// Create new expander for both vertices
|
|
recourse += self.create_new_expander(level_idx, &[u, v]);
|
|
}
|
|
}
|
|
|
|
recourse
|
|
}
|
|
|
|
/// Update a level for edge deletion
|
|
fn update_level_for_delete(&mut self, level_idx: usize, u: VertexId, v: VertexId) -> u64 {
|
|
if level_idx >= self.levels.len() {
|
|
return 0;
|
|
}
|
|
|
|
let mut recourse = 0u64;
|
|
|
|
// Get expanders containing u and v
|
|
let exp_u = self.levels[level_idx].vertex_expander.get(&u).copied();
|
|
let exp_v = self.levels[level_idx].vertex_expander.get(&v).copied();
|
|
|
|
if let (Some(eu), Some(ev)) = (exp_u, exp_v) {
|
|
if eu == ev {
|
|
// Same expander - check if it needs to split
|
|
recourse += self.check_and_split_expander(level_idx, eu);
|
|
} else {
|
|
// Different expanders - update boundary
|
|
recourse += 2;
|
|
self.update_expander_properties(level_idx, eu);
|
|
self.update_expander_properties(level_idx, ev);
|
|
}
|
|
}
|
|
|
|
recourse
|
|
}
|
|
|
|
/// Update properties of an expander
|
|
fn update_expander_properties(&mut self, level_idx: usize, exp_id: u64) {
|
|
if level_idx >= self.levels.len() {
|
|
return;
|
|
}
|
|
|
|
// Get vertices and phi first
|
|
let (vertices, phi) = match self.levels[level_idx].expanders.get(&exp_id) {
|
|
Some(e) => (e.vertices.clone(), self.levels[level_idx].phi),
|
|
None => return,
|
|
};
|
|
|
|
let volume: usize = vertices.iter().map(|&v| self.degree(v)).sum();
|
|
let boundary_size = self.count_boundary(&vertices);
|
|
|
|
// Check if still valid expander
|
|
let expansion = if volume > 0 {
|
|
boundary_size as f64 / volume as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
let is_valid = expansion >= phi * 0.3;
|
|
|
|
if let Some(expander) = self.levels[level_idx].expanders.get_mut(&exp_id) {
|
|
expander.volume = volume;
|
|
expander.boundary_size = boundary_size;
|
|
expander.is_valid_expander = is_valid;
|
|
}
|
|
}
|
|
|
|
/// Try to add a vertex to an expander
|
|
fn try_add_to_expander(&mut self, level_idx: usize, v: VertexId, exp_id: u64) -> u64 {
|
|
if level_idx >= self.levels.len() {
|
|
return 0;
|
|
}
|
|
|
|
// Check if adding would violate expansion
|
|
let (can_add, volume, boundary) = {
|
|
let level = &self.levels[level_idx];
|
|
if let Some(expander) = level.expanders.get(&exp_id) {
|
|
let mut test_vertices = expander.vertices.clone();
|
|
test_vertices.insert(v);
|
|
|
|
let volume: usize = test_vertices.iter().map(|&u| self.degree(u)).sum();
|
|
let boundary = self.count_boundary(&test_vertices);
|
|
|
|
let expansion = if volume > 0 {
|
|
boundary as f64 / volume as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
(expansion >= level.phi * 0.3, volume, boundary)
|
|
} else {
|
|
(false, 0, 0)
|
|
}
|
|
};
|
|
|
|
if can_add {
|
|
let level = &mut self.levels[level_idx];
|
|
if let Some(expander) = level.expanders.get_mut(&exp_id) {
|
|
expander.vertices.insert(v);
|
|
expander.volume = volume;
|
|
expander.boundary_size = boundary;
|
|
}
|
|
level.vertex_expander.insert(v, exp_id);
|
|
1
|
|
} else {
|
|
self.create_new_expander(level_idx, &[v])
|
|
}
|
|
}
|
|
|
|
/// Create a new expander for vertices
|
|
fn create_new_expander(&mut self, level_idx: usize, vertices: &[VertexId]) -> u64 {
|
|
if level_idx >= self.levels.len() {
|
|
return 0;
|
|
}
|
|
|
|
let vertex_set: HashSet<_> = vertices.iter().copied().collect();
|
|
let volume: usize = vertex_set.iter().map(|&v| self.degree(v)).sum();
|
|
let boundary_size = self.count_boundary(&vertex_set);
|
|
|
|
let level = &mut self.levels[level_idx];
|
|
let id = level.next_id;
|
|
level.next_id += 1;
|
|
|
|
let expander = LevelExpander {
|
|
id,
|
|
vertices: vertex_set.clone(),
|
|
boundary_size,
|
|
volume,
|
|
internal_min_cut: f64::INFINITY,
|
|
is_valid_expander: true,
|
|
parent_id: None,
|
|
children_ids: Vec::new(),
|
|
};
|
|
|
|
for &v in &vertex_set {
|
|
level.vertex_expander.insert(v, id);
|
|
}
|
|
|
|
level.expanders.insert(id, expander);
|
|
|
|
vertices.len() as u64
|
|
}
|
|
|
|
/// Check if an expander needs to split after edge deletion
|
|
fn check_and_split_expander(&mut self, level_idx: usize, exp_id: u64) -> u64 {
|
|
if level_idx >= self.levels.len() {
|
|
return 0;
|
|
}
|
|
|
|
// Check expansion property
|
|
let needs_split = {
|
|
let level = &self.levels[level_idx];
|
|
if let Some(expander) = level.expanders.get(&exp_id) {
|
|
let expansion = if expander.volume > 0 {
|
|
expander.boundary_size as f64 / expander.volume as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
expansion < level.phi * 0.2
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
|
|
if needs_split {
|
|
// For now, just mark as invalid and update properties
|
|
// A full split would require more complex logic
|
|
self.update_expander_properties(level_idx, exp_id);
|
|
2
|
|
} else {
|
|
self.update_expander_properties(level_idx, exp_id);
|
|
1
|
|
}
|
|
}
|
|
|
|
/// Update minimum cut incrementally
|
|
fn update_min_cut_incremental(&mut self, u: VertexId, v: VertexId, is_insert: bool) {
|
|
// Use LocalKCut for local cut discovery
|
|
if let Some(ref lkc) = self.local_kcut {
|
|
let cuts_u = lkc.query(u);
|
|
let cuts_v = lkc.query(v);
|
|
|
|
let mut min_local = f64::INFINITY;
|
|
|
|
for cut in cuts_u.iter().chain(cuts_v.iter()) {
|
|
if cut.cut_value < min_local {
|
|
min_local = cut.cut_value;
|
|
}
|
|
}
|
|
|
|
if is_insert {
|
|
// Edge insertion can only increase cuts
|
|
// But might enable new paths that reduce other cuts
|
|
self.current_min_cut = self.current_min_cut.min(min_local);
|
|
} else {
|
|
// Edge deletion might decrease the min cut
|
|
if min_local < self.current_min_cut * 1.5 {
|
|
// Need to verify more carefully
|
|
self.recompute_min_cut();
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback to full recomputation
|
|
self.recompute_min_cut();
|
|
}
|
|
}
|
|
|
|
/// Recompute the minimum cut from scratch
|
|
fn recompute_min_cut(&mut self) {
|
|
if self.edges.is_empty() {
|
|
self.current_min_cut = f64::INFINITY;
|
|
return;
|
|
}
|
|
|
|
let mut min_cut = f64::INFINITY;
|
|
|
|
// Check all level boundaries
|
|
for level in &self.levels {
|
|
for expander in level.expanders.values() {
|
|
// Boundary cut value
|
|
let boundary_cut = expander.boundary_size as f64;
|
|
min_cut = min_cut.min(boundary_cut);
|
|
|
|
// Internal cut (from cached value)
|
|
min_cut = min_cut.min(expander.internal_min_cut);
|
|
}
|
|
}
|
|
|
|
// Also query LocalKCut for local cuts
|
|
if let Some(ref lkc) = self.local_kcut {
|
|
for v in self.adjacency.keys().take(10) {
|
|
let cuts = lkc.query(*v);
|
|
for cut in cuts {
|
|
min_cut = min_cut.min(cut.cut_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.current_min_cut = min_cut;
|
|
}
|
|
|
|
/// Update recourse statistics
|
|
fn update_recourse_stats(&mut self, recourse: u64, time_us: f64) {
|
|
self.recourse_stats.total_recourse += recourse;
|
|
self.recourse_stats.num_updates += 1;
|
|
self.recourse_stats.max_single_recourse =
|
|
self.recourse_stats.max_single_recourse.max(recourse);
|
|
|
|
// Update average time
|
|
let n = self.recourse_stats.num_updates as f64;
|
|
self.recourse_stats.avg_update_time_us =
|
|
(self.recourse_stats.avg_update_time_us * (n - 1.0) + time_us) / n;
|
|
|
|
// Update per-level recourse
|
|
self.recourse_stats.recourse_per_level = self.levels.iter().map(|l| l.recourse).collect();
|
|
}
|
|
|
|
// === Helper methods ===
|
|
|
|
fn edge_key(u: VertexId, v: VertexId) -> (VertexId, VertexId) {
|
|
if u < v {
|
|
(u, v)
|
|
} else {
|
|
(v, u)
|
|
}
|
|
}
|
|
|
|
fn get_weight(&self, u: VertexId, v: VertexId) -> Option<Weight> {
|
|
self.adjacency.get(&u).and_then(|n| n.get(&v).copied())
|
|
}
|
|
|
|
fn degree(&self, v: VertexId) -> usize {
|
|
self.adjacency.get(&v).map_or(0, |n| n.len())
|
|
}
|
|
|
|
fn neighbors(&self, v: VertexId) -> Vec<(VertexId, Weight)> {
|
|
self.adjacency
|
|
.get(&v)
|
|
.map(|n| n.iter().map(|(&v, &w)| (v, w)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn count_boundary(&self, vertices: &HashSet<VertexId>) -> usize {
|
|
let mut boundary = 0;
|
|
for &v in vertices {
|
|
for (neighbor, _) in self.neighbors(v) {
|
|
if !vertices.contains(&neighbor) {
|
|
boundary += 1;
|
|
}
|
|
}
|
|
}
|
|
boundary
|
|
}
|
|
|
|
fn expanders_adjacent_at_level(&self, level_idx: usize, exp1: u64, exp2: u64) -> bool {
|
|
if level_idx >= self.levels.len() {
|
|
return false;
|
|
}
|
|
|
|
let level = &self.levels[level_idx];
|
|
|
|
let e1 = match level.expanders.get(&exp1) {
|
|
Some(e) => e,
|
|
None => return false,
|
|
};
|
|
let e2 = match level.expanders.get(&exp2) {
|
|
Some(e) => e,
|
|
None => return false,
|
|
};
|
|
|
|
// Check if any vertex in e1 has a neighbor in e2
|
|
for &v in &e1.vertices {
|
|
for (neighbor, _) in self.neighbors(v) {
|
|
if e2.vertices.contains(&neighbor) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// === Public API ===
|
|
|
|
/// Get the current minimum cut value
|
|
pub fn min_cut_value(&self) -> f64 {
|
|
self.current_min_cut
|
|
}
|
|
|
|
/// Get detailed minimum cut result
|
|
pub fn min_cut(&self) -> MinCutQueryResult {
|
|
MinCutQueryResult {
|
|
value: self.current_min_cut,
|
|
cut_edges: None, // Would need more work to track
|
|
partition: None,
|
|
is_exact: true,
|
|
complexity_verified: self.recourse_stats.is_subpolynomial(self.num_vertices),
|
|
}
|
|
}
|
|
|
|
/// Get configuration
|
|
pub fn config(&self) -> &SubpolyConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Get number of vertices
|
|
pub fn num_vertices(&self) -> usize {
|
|
self.num_vertices
|
|
}
|
|
|
|
/// Get number of edges
|
|
pub fn num_edges(&self) -> usize {
|
|
self.num_edges
|
|
}
|
|
|
|
/// Get number of hierarchy levels
|
|
pub fn num_levels(&self) -> usize {
|
|
self.levels.len()
|
|
}
|
|
|
|
/// Get recourse statistics
|
|
pub fn recourse_stats(&self) -> &RecourseStats {
|
|
&self.recourse_stats
|
|
}
|
|
|
|
/// Get hierarchy statistics
|
|
pub fn hierarchy_stats(&self) -> HierarchyStatistics {
|
|
HierarchyStatistics {
|
|
num_levels: self.levels.len(),
|
|
expanders_per_level: self.levels.iter().map(|l| l.expanders.len()).collect(),
|
|
total_expanders: self.levels.iter().map(|l| l.expanders.len()).sum(),
|
|
avg_expander_size: if self.levels[0].expanders.is_empty() {
|
|
0.0
|
|
} else {
|
|
self.levels[0]
|
|
.expanders
|
|
.values()
|
|
.map(|e| e.vertices.len())
|
|
.sum::<usize>() as f64
|
|
/ self.levels[0].expanders.len() as f64
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Check if updates are subpolynomial
|
|
pub fn is_subpolynomial(&self) -> bool {
|
|
self.recourse_stats.is_subpolynomial(self.num_vertices)
|
|
}
|
|
|
|
/// Certify cuts using LocalKCut verification
|
|
pub fn certify_cuts(&mut self) {
|
|
// First collect all expander info (exp_id, vertices)
|
|
let mut expander_data: Vec<(usize, u64, HashSet<VertexId>)> = Vec::new();
|
|
for level_idx in 0..self.levels.len() {
|
|
for (&exp_id, expander) in &self.levels[level_idx].expanders {
|
|
expander_data.push((level_idx, exp_id, expander.vertices.clone()));
|
|
}
|
|
}
|
|
|
|
// Now process each expander with immutable self
|
|
let mut updates: Vec<(usize, u64, f64)> = Vec::new();
|
|
|
|
if let Some(ref lkc) = self.local_kcut {
|
|
for (level_idx, exp_id, vertices) in &expander_data {
|
|
// Sample boundary vertices
|
|
let boundary_verts: Vec<_> = vertices
|
|
.iter()
|
|
.filter(|&&v| self.neighbors(v).iter().any(|(n, _)| !vertices.contains(n)))
|
|
.take(5)
|
|
.copied()
|
|
.collect();
|
|
|
|
let mut min_internal_cut = f64::INFINITY;
|
|
|
|
for v in boundary_verts {
|
|
let cuts = lkc.query(v);
|
|
for cut in cuts {
|
|
// Check if cut is internal to expander
|
|
let is_internal = cut.vertices.iter().all(|u| vertices.contains(u));
|
|
|
|
if is_internal {
|
|
min_internal_cut = min_internal_cut.min(cut.cut_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
updates.push((*level_idx, *exp_id, min_internal_cut));
|
|
}
|
|
}
|
|
|
|
// Now apply all updates
|
|
for (level_idx, exp_id, min_cut) in updates {
|
|
if let Some(expander) = self.levels[level_idx].expanders.get_mut(&exp_id) {
|
|
expander.internal_min_cut = min_cut;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of a minimum cut query
|
|
#[derive(Debug, Clone)]
|
|
pub struct MinCutQueryResult {
|
|
/// The minimum cut value
|
|
pub value: f64,
|
|
/// Edges in the cut (if computed)
|
|
pub cut_edges: Option<Vec<(VertexId, VertexId)>>,
|
|
/// Partition (S, T) (if computed)
|
|
pub partition: Option<(Vec<VertexId>, Vec<VertexId>)>,
|
|
/// Whether this is an exact result
|
|
pub is_exact: bool,
|
|
/// Whether subpolynomial complexity is verified
|
|
pub complexity_verified: bool,
|
|
}
|
|
|
|
/// Statistics about the hierarchy
|
|
#[derive(Debug, Clone)]
|
|
pub struct HierarchyStatistics {
|
|
/// Number of levels
|
|
pub num_levels: usize,
|
|
/// Expanders at each level
|
|
pub expanders_per_level: Vec<usize>,
|
|
/// Total expanders across all levels
|
|
pub total_expanders: usize,
|
|
/// Average expander size at base level
|
|
pub avg_expander_size: f64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_subpoly_config_default() {
|
|
let config = SubpolyConfig::default();
|
|
assert!(config.phi > 0.0);
|
|
assert!(config.lambda_max > 0);
|
|
assert!(config.target_levels > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_subpoly_config_for_size() {
|
|
let config = SubpolyConfig::for_size(1_000_000);
|
|
assert!(config.phi < 0.1);
|
|
assert!(config.lambda_max > 100);
|
|
assert!(config.target_levels >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_empty() {
|
|
let mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
assert_eq!(mincut.num_vertices(), 0);
|
|
assert_eq!(mincut.num_edges(), 0);
|
|
assert_eq!(mincut.min_cut_value(), f64::INFINITY);
|
|
}
|
|
|
|
#[test]
|
|
fn test_insert_edges() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
assert_eq!(mincut.num_vertices(), 3);
|
|
assert_eq!(mincut.num_edges(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_hierarchy() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Build a path graph
|
|
for i in 0..10 {
|
|
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
|
}
|
|
|
|
mincut.build();
|
|
|
|
assert!(mincut.num_levels() >= 2);
|
|
let stats = mincut.hierarchy_stats();
|
|
assert!(stats.total_expanders > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_min_cut_triangle() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
mincut.build();
|
|
|
|
assert!(mincut.min_cut_value() <= 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_min_cut_bridge() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Two triangles connected by a bridge
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
mincut.insert_edge(3, 4, 1.0).unwrap(); // Bridge
|
|
|
|
mincut.insert_edge(4, 5, 1.0).unwrap();
|
|
mincut.insert_edge(5, 6, 1.0).unwrap();
|
|
mincut.insert_edge(6, 4, 1.0).unwrap();
|
|
|
|
mincut.build();
|
|
|
|
// Min cut should be 1 (the bridge)
|
|
assert!(mincut.min_cut_value() <= 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_incremental_updates() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Build initial graph
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
mincut.build();
|
|
|
|
let initial_cut = mincut.min_cut_value();
|
|
|
|
// Add more edges
|
|
mincut.insert_edge(3, 4, 1.0).unwrap();
|
|
mincut.insert_edge(4, 5, 1.0).unwrap();
|
|
|
|
// Cut might have changed
|
|
assert!(mincut.min_cut_value() <= initial_cut * 2.0);
|
|
|
|
// Check recourse tracking
|
|
let stats = mincut.recourse_stats();
|
|
assert!(stats.num_updates > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_delete_edge() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
mincut.build();
|
|
|
|
mincut.delete_edge(1, 2).unwrap();
|
|
|
|
assert_eq!(mincut.num_edges(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_recourse_stats() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Build graph
|
|
for i in 0..20 {
|
|
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
|
}
|
|
|
|
mincut.build();
|
|
|
|
// Do updates
|
|
mincut.insert_edge(0, 10, 1.0).unwrap();
|
|
mincut.insert_edge(5, 15, 1.0).unwrap();
|
|
|
|
let stats = mincut.recourse_stats();
|
|
assert!(stats.num_updates >= 2);
|
|
assert!(stats.amortized_recourse() >= 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_subpolynomial() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Build small graph
|
|
for i in 0..10 {
|
|
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
|
}
|
|
|
|
mincut.build();
|
|
|
|
// Do some updates
|
|
mincut.insert_edge(0, 5, 1.0).unwrap();
|
|
|
|
// Should be subpolynomial for small graph
|
|
assert!(mincut.is_subpolynomial());
|
|
}
|
|
|
|
#[test]
|
|
fn test_certify_cuts() {
|
|
let mut mincut = SubpolynomialMinCut::new(SubpolyConfig::default());
|
|
|
|
// Build graph
|
|
mincut.insert_edge(1, 2, 1.0).unwrap();
|
|
mincut.insert_edge(2, 3, 1.0).unwrap();
|
|
mincut.insert_edge(3, 1, 1.0).unwrap();
|
|
|
|
mincut.build();
|
|
mincut.certify_cuts();
|
|
|
|
// Should complete without panic
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_graph() {
|
|
let mut mincut = SubpolynomialMinCut::for_size(1000);
|
|
|
|
// Build a larger graph
|
|
for i in 0..100 {
|
|
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
|
}
|
|
// Add some cross edges
|
|
for i in 0..10 {
|
|
mincut.insert_edge(i * 10, i * 10 + 50, 1.0).unwrap();
|
|
}
|
|
|
|
mincut.build();
|
|
|
|
let stats = mincut.hierarchy_stats();
|
|
assert!(stats.num_levels >= 2);
|
|
|
|
// Test updates
|
|
mincut.insert_edge(25, 75, 1.0).unwrap();
|
|
|
|
assert!(mincut.recourse_stats().num_updates > 0);
|
|
}
|
|
}
|