use crate::error::{MinCutError, Result};
use crate::graph::{DynamicGraph, Edge, EdgeId, VertexId, Weight};
use crate::jtree::JTreeError;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct SparsifierConfig {
pub epsilon: f64,
pub max_recourse_per_update: usize,
pub lazy_repair: bool,
pub seed: Option<u64>,
}
impl Default for SparsifierConfig {
fn default() -> Self {
Self {
epsilon: 0.1,
max_recourse_per_update: 0,
lazy_repair: true,
seed: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SparsifierStatistics {
pub num_forests: usize,
pub sparse_edge_count: usize,
pub compression_ratio: f64,
pub total_recourse: usize,
pub max_single_recourse: usize,
pub vertex_splits: usize,
pub lazy_repairs: usize,
}
#[derive(Debug, Clone)]
pub struct VertexSplitResult {
pub new_vertices: Vec<VertexId>,
pub recourse: usize,
pub forests_repaired: usize,
}
#[derive(Debug, Clone)]
pub struct RecourseTracker {
history: Vec<usize>,
total: usize,
max_single: usize,
theoretical_bound: usize,
}
impl RecourseTracker {
pub fn new(n: usize, epsilon: f64) -> Self {
let log_n = (n as f64).ln().max(1.0);
let bound = ((log_n * log_n) / (epsilon * epsilon)).ceil() as usize;
Self {
history: Vec::new(),
total: 0,
max_single: 0,
theoretical_bound: bound,
}
}
pub fn record(&mut self, recourse: usize) {
self.history.push(recourse);
self.total += recourse;
self.max_single = self.max_single.max(recourse);
}
pub fn total(&self) -> usize {
self.total
}
pub fn max_single(&self) -> usize {
self.max_single
}
pub fn is_within_bound(&self) -> bool {
self.max_single <= self.theoretical_bound
}
pub fn theoretical_bound(&self) -> usize {
self.theoretical_bound
}
pub fn average(&self) -> f64 {
if self.history.is_empty() {
0.0
} else {
self.total as f64 / self.history.len() as f64
}
}
pub fn num_updates(&self) -> usize {
self.history.len()
}
}
#[derive(Debug, Clone)]
struct Forest {
id: usize,
edges: HashSet<(VertexId, VertexId)>,
parent: HashMap<VertexId, VertexId>,
roots: HashSet<VertexId>,
needs_repair: bool,
}
impl Forest {
fn new(id: usize) -> Self {
Self {
id,
edges: HashSet::new(),
parent: HashMap::new(),
roots: HashSet::new(),
needs_repair: false,
}
}
fn add_edge(&mut self, u: VertexId, v: VertexId) -> bool {
let key = if u <= v { (u, v) } else { (v, u) };
self.edges.insert(key)
}
fn remove_edge(&mut self, u: VertexId, v: VertexId) -> bool {
let key = if u <= v { (u, v) } else { (v, u) };
self.edges.remove(&key)
}
fn has_edge(&self, u: VertexId, v: VertexId) -> bool {
let key = if u <= v { (u, v) } else { (v, u) };
self.edges.contains(&key)
}
fn edge_count(&self) -> usize {
self.edges.len()
}
}
#[derive(Debug)]
pub struct ForestPacking {
forests: Vec<Forest>,
config: SparsifierConfig,
vertex_count: usize,
rng_state: u64,
}
impl ForestPacking {
pub fn new(vertex_count: usize, config: SparsifierConfig) -> Self {
let log_n = (vertex_count as f64).ln().max(1.0);
let num_forests = ((log_n / (config.epsilon * config.epsilon)).ceil() as usize).max(1);
let forests = (0..num_forests).map(Forest::new).collect();
let rng_state = config.seed.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(12345)
});
Self {
forests,
config,
vertex_count,
rng_state,
}
}
pub fn num_forests(&self) -> usize {
self.forests.len()
}
fn next_random(&mut self) -> u64 {
self.rng_state ^= self.rng_state << 13;
self.rng_state ^= self.rng_state >> 7;
self.rng_state ^= self.rng_state << 17;
self.rng_state
}
pub fn sample_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Vec<usize> {
let mut sampled_forests = Vec::new();
let sample_prob = (weight / (weight + 1.0)).min(1.0);
let num_forests = self.forests.len();
let random_values: Vec<f64> = (0..num_forests)
.map(|_| (self.next_random() % 1000) as f64 / 1000.0)
.collect();
for (i, forest) in self.forests.iter_mut().enumerate() {
if random_values[i] < sample_prob {
if forest.add_edge(u, v) {
sampled_forests.push(i);
}
}
}
sampled_forests
}
pub fn remove_edge(&mut self, u: VertexId, v: VertexId) -> Vec<usize> {
let mut removed_from = Vec::new();
for (i, forest) in self.forests.iter_mut().enumerate() {
if forest.remove_edge(u, v) {
removed_from.push(i);
forest.needs_repair = true;
}
}
removed_from
}
pub fn split_vertex(
&mut self,
v: VertexId,
v1: VertexId,
v2: VertexId,
partition: &[EdgeId],
) -> Vec<usize> {
let mut affected = Vec::new();
for (i, forest) in self.forests.iter_mut().enumerate() {
let forest_edges: Vec<_> = forest.edges.iter().copied().collect();
let mut was_affected = false;
for (a, b) in forest_edges {
if a == v || b == v {
was_affected = true;
forest.needs_repair = true;
}
}
if was_affected {
affected.push(i);
}
}
affected
}
pub fn repair_forest(&mut self, forest_id: usize) -> usize {
if forest_id >= self.forests.len() {
return 0;
}
let forest = &mut self.forests[forest_id];
if !forest.needs_repair {
return 0;
}
forest.needs_repair = false;
forest.edge_count()
}
pub fn total_edges(&self) -> usize {
self.forests.iter().map(|f| f.edge_count()).sum()
}
pub fn needs_repair(&self) -> bool {
self.forests.iter().any(|f| f.needs_repair)
}
pub fn forests_needing_repair(&self) -> Vec<usize> {
self.forests
.iter()
.enumerate()
.filter(|(_, f)| f.needs_repair)
.map(|(i, _)| i)
.collect()
}
}
pub struct DynamicCutSparsifier {
forest_packing: ForestPacking,
sparse_edges: HashMap<(VertexId, VertexId), Weight>,
original_weights: HashMap<(VertexId, VertexId), Weight>,
config: SparsifierConfig,
recourse: RecourseTracker,
stats: SparsifierStatistics,
last_recourse: usize,
}
impl DynamicCutSparsifier {
pub fn build(graph: &DynamicGraph, config: SparsifierConfig) -> Result<Self> {
let n = graph.num_vertices();
let forest_packing = ForestPacking::new(n, config.clone());
let recourse = RecourseTracker::new(n, config.epsilon);
let mut sparsifier = Self {
forest_packing,
sparse_edges: HashMap::new(),
original_weights: HashMap::new(),
config,
recourse,
stats: SparsifierStatistics::default(),
last_recourse: 0,
};
for edge in graph.edges() {
sparsifier.insert_edge(edge.source, edge.target, edge.weight)?;
}
sparsifier.stats.num_forests = sparsifier.forest_packing.num_forests();
Ok(sparsifier)
}
fn edge_key(u: VertexId, v: VertexId) -> (VertexId, VertexId) {
if u <= v {
(u, v)
} else {
(v, u)
}
}
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<()> {
let key = Self::edge_key(u, v);
self.original_weights.insert(key, weight);
let sampled = self.forest_packing.sample_edge(u, v, weight);
if !sampled.is_empty() {
*self.sparse_edges.entry(key).or_insert(0.0) += weight;
}
self.last_recourse = sampled.len();
self.recourse.record(self.last_recourse);
self.update_stats();
Ok(())
}
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
let key = Self::edge_key(u, v);
self.original_weights.remove(&key);
let removed_from = self.forest_packing.remove_edge(u, v);
self.sparse_edges.remove(&key);
let mut total_recourse = removed_from.len();
if !self.config.lazy_repair {
for forest_id in &removed_from {
total_recourse += self.forest_packing.repair_forest(*forest_id);
}
}
self.last_recourse = total_recourse;
self.recourse.record(self.last_recourse);
self.update_stats();
Ok(())
}
pub fn split_vertex(
&mut self,
v: VertexId,
v1: VertexId,
v2: VertexId,
partition: &[EdgeId],
) -> Result<VertexSplitResult> {
let affected_forests = self.forest_packing.split_vertex(v, v1, v2, partition);
let mut total_recourse = 0;
let mut forests_repaired = 0;
if !self.config.lazy_repair {
for forest_id in &affected_forests {
let repaired = self.forest_packing.repair_forest(*forest_id);
total_recourse += repaired;
if repaired > 0 {
forests_repaired += 1;
}
}
}
self.last_recourse = total_recourse;
self.recourse.record(total_recourse);
self.stats.vertex_splits += 1;
self.stats.lazy_repairs += forests_repaired;
self.update_stats();
if self.config.max_recourse_per_update > 0
&& total_recourse > self.config.max_recourse_per_update
{
return Err(JTreeError::RecourseExceeded {
actual: total_recourse,
limit: self.config.max_recourse_per_update,
}
.into());
}
Ok(VertexSplitResult {
new_vertices: vec![v1, v2],
recourse: total_recourse,
forests_repaired,
})
}
pub fn perform_lazy_repairs(&mut self) -> usize {
let mut total_repaired = 0;
for forest_id in self.forest_packing.forests_needing_repair() {
let repaired = self.forest_packing.repair_forest(forest_id);
total_repaired += repaired;
if repaired > 0 {
self.stats.lazy_repairs += 1;
}
}
total_repaired
}
pub fn last_recourse(&self) -> usize {
self.last_recourse
}
pub fn recourse_tracker(&self) -> &RecourseTracker {
&self.recourse
}
pub fn statistics(&self) -> SparsifierStatistics {
self.stats.clone()
}
pub fn sparse_edges(&self) -> impl Iterator<Item = (VertexId, VertexId, Weight)> + '_ {
self.sparse_edges.iter().map(|(&(u, v), &w)| (u, v, w))
}
pub fn sparse_edge_count(&self) -> usize {
self.sparse_edges.len()
}
pub fn compression_ratio(&self) -> f64 {
if self.original_weights.is_empty() {
1.0
} else {
self.sparse_edges.len() as f64 / self.original_weights.len() as f64
}
}
fn update_stats(&mut self) {
self.stats.sparse_edge_count = self.sparse_edges.len();
self.stats.compression_ratio = self.compression_ratio();
self.stats.total_recourse = self.recourse.total();
self.stats.max_single_recourse = self.recourse.max_single();
}
pub fn is_within_recourse_bound(&self) -> bool {
self.recourse.is_within_bound()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_graph() -> DynamicGraph {
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap();
graph.insert_edge(4, 5, 1.0).unwrap();
graph
}
#[test]
fn test_recourse_tracker() {
let mut tracker = RecourseTracker::new(100, 0.1);
tracker.record(5);
tracker.record(3);
tracker.record(10);
assert_eq!(tracker.total(), 18);
assert_eq!(tracker.max_single(), 10);
assert_eq!(tracker.num_updates(), 3);
assert!((tracker.average() - 6.0).abs() < 0.001);
}
#[test]
fn test_forest_packing_creation() {
let config = SparsifierConfig::default();
let packing = ForestPacking::new(100, config);
assert!(packing.num_forests() > 0);
assert_eq!(packing.total_edges(), 0);
}
#[test]
fn test_forest_edge_operations() {
let mut forest = Forest::new(0);
assert!(forest.add_edge(1, 2));
assert!(forest.has_edge(1, 2));
assert!(forest.has_edge(2, 1));
assert!(!forest.add_edge(1, 2));
assert!(forest.remove_edge(1, 2));
assert!(!forest.has_edge(1, 2));
}
#[test]
fn test_sparsifier_build() {
let graph = create_test_graph();
let config = SparsifierConfig::default();
let sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
let stats = sparsifier.statistics();
assert!(stats.num_forests > 0);
}
#[test]
fn test_sparsifier_insert_delete() {
let graph = create_test_graph();
let config = SparsifierConfig::default();
let mut sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
let initial_edges = sparsifier.sparse_edge_count();
graph.insert_edge(1, 5, 2.0).unwrap();
sparsifier.insert_edge(1, 5, 2.0).unwrap();
graph.delete_edge(2, 3).unwrap();
sparsifier.delete_edge(2, 3).unwrap();
assert!(sparsifier.recourse_tracker().num_updates() > 0);
}
#[test]
fn test_vertex_split() {
let graph = create_test_graph();
let config = SparsifierConfig {
lazy_repair: false, ..Default::default()
};
let mut sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
let result = sparsifier.split_vertex(3, 6, 7, &[]).unwrap();
assert_eq!(result.new_vertices, vec![6, 7]);
assert!(sparsifier.statistics().vertex_splits > 0);
}
#[test]
fn test_lazy_repair() {
let graph = create_test_graph();
let config = SparsifierConfig {
lazy_repair: true,
..Default::default()
};
let mut sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
sparsifier.delete_edge(2, 3).unwrap();
let pending = sparsifier.forest_packing.needs_repair();
let repaired = sparsifier.perform_lazy_repairs();
assert!(!sparsifier.forest_packing.needs_repair());
}
#[test]
fn test_recourse_bound_check() {
let graph = create_test_graph();
let config = SparsifierConfig::default();
let sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
let _ = sparsifier.is_within_recourse_bound();
}
#[test]
fn test_compression_ratio() {
let graph = create_test_graph();
let config = SparsifierConfig {
epsilon: 0.5, ..Default::default()
};
let sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
let ratio = sparsifier.compression_ratio();
assert!(ratio >= 0.0);
}
#[test]
fn test_sparsifier_statistics() {
let graph = create_test_graph();
let config = SparsifierConfig::default();
let mut sparsifier = DynamicCutSparsifier::build(&graph, config).unwrap();
sparsifier.insert_edge(1, 5, 1.0).unwrap();
sparsifier.delete_edge(1, 2).unwrap();
let stats = sparsifier.statistics();
assert!(stats.num_forests > 0);
assert!(stats.total_recourse > 0);
}
#[test]
fn test_config_default() {
let config = SparsifierConfig::default();
assert!((config.epsilon - 0.1).abs() < 0.001);
assert!(config.lazy_repair);
assert_eq!(config.max_recourse_per_update, 0);
}
}