ruvector_mincut/algorithm/
approximate.rs

1//! Approximate Min-Cut for All Cut Sizes
2//!
3//! Implementation based on "Approximate Min-Cut in All Cut Sizes"
4//! (SODA 2025, arXiv:2412.15069).
5//!
6//! # Key Innovation
7//!
8//! Uses spectral sparsification with edge sampling to achieve (1+ε)-approximate
9//! minimum cuts for ANY cut size, not just small cuts.
10//!
11//! # Time Complexity
12//!
13//! - Preprocessing: O(m log² n / ε²)
14//! - Query: O(n polylog n / ε²)
15//!
16//! # Algorithm Overview
17//!
18//! 1. Compute effective resistances for all edges
19//! 2. Sample edges with probability proportional to resistance × weight
20//! 3. Build sparsifier with O(n log n / ε²) edges
21//! 4. Run exact min-cut on sparsifier (feasible due to small size)
22
23use std::collections::{HashMap, HashSet, VecDeque};
24use crate::graph::VertexId;
25
26/// Configuration for approximate min-cut
27#[derive(Debug, Clone)]
28pub struct ApproxMinCutConfig {
29    /// Approximation parameter (0 < ε ≤ 1)
30    pub epsilon: f64,
31    /// Number of sparsifier samples (higher = more accurate)
32    pub num_samples: usize,
33    /// Seed for reproducibility
34    pub seed: u64,
35}
36
37impl Default for ApproxMinCutConfig {
38    fn default() -> Self {
39        Self {
40            epsilon: 0.1,
41            num_samples: 3,
42            seed: 42,
43        }
44    }
45}
46
47/// Edge with weight for the sparsifier
48#[derive(Debug, Clone, Copy, PartialEq)]
49struct WeightedEdge {
50    u: VertexId,
51    v: VertexId,
52    weight: f64,
53}
54
55impl WeightedEdge {
56    fn new(u: VertexId, v: VertexId, weight: f64) -> Self {
57        let (u, v) = if u < v { (u, v) } else { (v, u) };
58        Self { u, v, weight }
59    }
60
61    fn endpoints(&self) -> (VertexId, VertexId) {
62        (self.u, self.v)
63    }
64}
65
66/// Spectral sparsifier for approximate min-cut
67#[derive(Debug)]
68struct SpectralSparsifier {
69    /// Edges in the sparsifier
70    edges: Vec<WeightedEdge>,
71    /// Vertex set
72    vertices: HashSet<VertexId>,
73    /// Adjacency map
74    adj: HashMap<VertexId, Vec<(VertexId, f64)>>,
75}
76
77impl SpectralSparsifier {
78    fn new() -> Self {
79        Self {
80            edges: Vec::new(),
81            vertices: HashSet::new(),
82            adj: HashMap::new(),
83        }
84    }
85
86    fn add_edge(&mut self, u: VertexId, v: VertexId, weight: f64) {
87        self.vertices.insert(u);
88        self.vertices.insert(v);
89        self.edges.push(WeightedEdge::new(u, v, weight));
90
91        self.adj.entry(u).or_default().push((v, weight));
92        self.adj.entry(v).or_default().push((u, weight));
93    }
94
95    fn vertex_count(&self) -> usize {
96        self.vertices.len()
97    }
98
99    fn edge_count(&self) -> usize {
100        self.edges.len()
101    }
102}
103
104/// Approximate minimum cut for all cut sizes
105///
106/// Achieves (1+ε)-approximation for any cut size using spectral sparsification.
107///
108/// # Example
109///
110/// ```ignore
111/// use ruvector_mincut::algorithm::approximate::ApproxMinCut;
112///
113/// let mut approx = ApproxMinCut::new(ApproxMinCutConfig::default());
114/// approx.insert_edge(0, 1, 1.0);
115/// approx.insert_edge(1, 2, 1.0);
116/// approx.insert_edge(2, 0, 1.0);
117///
118/// let result = approx.min_cut();
119/// assert!(result.value >= 2.0 * 0.9); // (1-ε) lower bound
120/// ```
121#[derive(Debug)]
122pub struct ApproxMinCut {
123    /// All edges in the graph
124    edges: Vec<WeightedEdge>,
125    /// Vertex set
126    vertices: HashSet<VertexId>,
127    /// Adjacency list
128    adj: HashMap<VertexId, Vec<(VertexId, f64)>>,
129    /// Effective resistances (computed lazily)
130    resistances: HashMap<(VertexId, VertexId), f64>,
131    /// Configuration
132    config: ApproxMinCutConfig,
133    /// Current minimum cut value
134    cached_min_cut: Option<f64>,
135    /// Statistics
136    stats: ApproxMinCutStats,
137}
138
139/// Statistics for approximate min-cut
140#[derive(Debug, Clone, Default)]
141pub struct ApproxMinCutStats {
142    /// Total insertions
143    pub insertions: u64,
144    /// Total deletions
145    pub deletions: u64,
146    /// Total queries
147    pub queries: u64,
148    /// Number of sparsifier rebuilds
149    pub rebuilds: u64,
150}
151
152/// Result of approximate min-cut query
153#[derive(Debug, Clone)]
154pub struct ApproxMinCutResult {
155    /// Approximate minimum cut value
156    pub value: f64,
157    /// Lower bound (value / (1+ε))
158    pub lower_bound: f64,
159    /// Upper bound (value * (1+ε))
160    pub upper_bound: f64,
161    /// Partition achieving the cut
162    pub partition: Option<(Vec<VertexId>, Vec<VertexId>)>,
163    /// Approximation ratio used
164    pub epsilon: f64,
165}
166
167impl ApproxMinCut {
168    /// Create new approximate min-cut structure
169    pub fn new(config: ApproxMinCutConfig) -> Self {
170        Self {
171            edges: Vec::new(),
172            vertices: HashSet::new(),
173            adj: HashMap::new(),
174            resistances: HashMap::new(),
175            config,
176            cached_min_cut: None,
177            stats: ApproxMinCutStats::default(),
178        }
179    }
180
181    /// Create with default configuration
182    pub fn with_epsilon(epsilon: f64) -> Self {
183        Self::new(ApproxMinCutConfig {
184            epsilon,
185            ..Default::default()
186        })
187    }
188
189    /// Insert an edge
190    pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: f64) {
191        self.stats.insertions += 1;
192        self.cached_min_cut = None; // Invalidate cache
193
194        let edge = WeightedEdge::new(u, v, weight);
195        self.edges.push(edge);
196        self.vertices.insert(u);
197        self.vertices.insert(v);
198
199        self.adj.entry(u).or_default().push((v, weight));
200        self.adj.entry(v).or_default().push((u, weight));
201
202        // Clear resistance cache
203        self.resistances.clear();
204    }
205
206    /// Delete an edge
207    pub fn delete_edge(&mut self, u: VertexId, v: VertexId) {
208        self.stats.deletions += 1;
209        self.cached_min_cut = None;
210
211        let key = if u < v { (u, v) } else { (v, u) };
212        self.edges.retain(|e| e.endpoints() != key);
213
214        // Update adjacency
215        if let Some(neighbors) = self.adj.get_mut(&u) {
216            neighbors.retain(|(neighbor, _)| *neighbor != v);
217        }
218        if let Some(neighbors) = self.adj.get_mut(&v) {
219            neighbors.retain(|(neighbor, _)| *neighbor != u);
220        }
221
222        self.resistances.clear();
223    }
224
225    /// Query approximate minimum cut
226    pub fn min_cut(&mut self) -> ApproxMinCutResult {
227        self.stats.queries += 1;
228
229        if self.edges.is_empty() {
230            return ApproxMinCutResult {
231                value: f64::INFINITY,
232                lower_bound: f64::INFINITY,
233                upper_bound: f64::INFINITY,
234                partition: None,
235                epsilon: self.config.epsilon,
236            };
237        }
238
239        // Use cached value if available
240        if let Some(cached) = self.cached_min_cut {
241            let lower = cached / (1.0 + self.config.epsilon);
242            let upper = cached * (1.0 + self.config.epsilon);
243            return ApproxMinCutResult {
244                value: cached,
245                lower_bound: lower,
246                upper_bound: upper,
247                partition: None,
248                epsilon: self.config.epsilon,
249            };
250        }
251
252        // Build sparsifier and compute min-cut
253        let value = self.compute_min_cut_via_sparsifier();
254        self.cached_min_cut = Some(value);
255
256        let lower = value / (1.0 + self.config.epsilon);
257        let upper = value * (1.0 + self.config.epsilon);
258
259        ApproxMinCutResult {
260            value,
261            lower_bound: lower,
262            upper_bound: upper,
263            partition: self.compute_partition(value),
264            epsilon: self.config.epsilon,
265        }
266    }
267
268    /// Get minimum cut value only
269    pub fn min_cut_value(&mut self) -> f64 {
270        self.min_cut().value
271    }
272
273    /// Check if graph is connected
274    pub fn is_connected(&self) -> bool {
275        if self.vertices.is_empty() {
276            return true;
277        }
278
279        let start = *self.vertices.iter().next().unwrap();
280        let mut visited = HashSet::new();
281        let mut queue = VecDeque::new();
282
283        queue.push_back(start);
284        visited.insert(start);
285
286        while let Some(current) = queue.pop_front() {
287            if let Some(neighbors) = self.adj.get(&current) {
288                for &(neighbor, _) in neighbors {
289                    if !visited.contains(&neighbor) {
290                        visited.insert(neighbor);
291                        queue.push_back(neighbor);
292                    }
293                }
294            }
295        }
296
297        visited.len() == self.vertices.len()
298    }
299
300    /// Get vertex count
301    pub fn vertex_count(&self) -> usize {
302        self.vertices.len()
303    }
304
305    /// Get edge count
306    pub fn edge_count(&self) -> usize {
307        self.edges.len()
308    }
309
310    /// Get statistics
311    pub fn stats(&self) -> &ApproxMinCutStats {
312        &self.stats
313    }
314
315    /// Compute min-cut via spectral sparsification
316    fn compute_min_cut_via_sparsifier(&mut self) -> f64 {
317        self.stats.rebuilds += 1;
318
319        if !self.is_connected() {
320            return 0.0;
321        }
322
323        // For small graphs, compute exactly
324        if self.edges.len() <= 50 {
325            return self.compute_exact_min_cut();
326        }
327
328        // Compute effective resistances
329        self.compute_effective_resistances();
330
331        // Build sparsifier(s) and take median
332        let mut estimates = Vec::new();
333        for i in 0..self.config.num_samples {
334            let sparsifier = self.build_sparsifier(self.config.seed + i as u64);
335            let cut = self.compute_sparsifier_min_cut(&sparsifier);
336            estimates.push(cut);
337        }
338
339        estimates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
340        estimates[estimates.len() / 2]
341    }
342
343    /// Compute effective resistances using BFS approximation
344    fn compute_effective_resistances(&mut self) {
345        if !self.resistances.is_empty() {
346            return;
347        }
348
349        // Approximate effective resistance using shortest path heuristic
350        // True spectral computation would use Laplacian solver
351        for edge in &self.edges {
352            let (u, v) = edge.endpoints();
353
354            // Approximate R_eff(u,v) ≈ 1/(min_degree) for connected pairs
355            let deg_u = self.adj.get(&u).map_or(1, |n| n.len().max(1));
356            let deg_v = self.adj.get(&v).map_or(1, |n| n.len().max(1));
357            let approx_resistance = 2.0 / (deg_u + deg_v) as f64;
358
359            self.resistances.insert((u, v), approx_resistance);
360        }
361    }
362
363    /// Build spectral sparsifier by sampling edges
364    fn build_sparsifier(&self, seed: u64) -> SpectralSparsifier {
365        let mut sparsifier = SpectralSparsifier::new();
366        let n = self.vertices.len();
367        let epsilon = self.config.epsilon;
368
369        // Target size: O(n log n / ε²)
370        let target_size = ((n as f64) * (n as f64).ln() / (epsilon * epsilon)).ceil() as usize;
371        let target_size = target_size.min(self.edges.len()).max(n);
372
373        // Simple deterministic hash for reproducibility
374        let hash = |i: usize| -> u64 {
375            let mut h = seed.wrapping_add(i as u64);
376            h = h.wrapping_mul(0x517cc1b727220a95);
377            h ^= h >> 32;
378            h
379        };
380
381        // Sample each edge with probability proportional to weight × resistance
382        let total_weight: f64 = self.edges.iter().map(|e| e.weight).sum();
383
384        for (i, edge) in self.edges.iter().enumerate() {
385            let (u, v) = edge.endpoints();
386            let resistance = self.resistances.get(&(u, v)).copied().unwrap_or(1.0);
387
388            // Sampling probability
389            let prob = (edge.weight * resistance * target_size as f64 / total_weight).min(1.0);
390
391            // Use hash for deterministic sampling
392            let rand_val = (hash(i) as f64) / (u64::MAX as f64);
393
394            if rand_val < prob {
395                // Rescale weight to preserve expected cut values
396                let new_weight = edge.weight / prob;
397                sparsifier.add_edge(u, v, new_weight);
398            }
399        }
400
401        // Ensure connectivity by adding spanning tree
402        self.ensure_sparsifier_connectivity(&mut sparsifier);
403
404        sparsifier
405    }
406
407    /// Ensure sparsifier is connected
408    fn ensure_sparsifier_connectivity(&self, sparsifier: &mut SpectralSparsifier) {
409        // BFS to find spanning tree edges
410        if self.vertices.is_empty() {
411            return;
412        }
413
414        let start = *self.vertices.iter().next().unwrap();
415        let mut visited = HashSet::new();
416        let mut queue = VecDeque::new();
417
418        queue.push_back(start);
419        visited.insert(start);
420
421        while let Some(current) = queue.pop_front() {
422            if let Some(neighbors) = self.adj.get(&current) {
423                for &(neighbor, weight) in neighbors {
424                    if !visited.contains(&neighbor) {
425                        visited.insert(neighbor);
426                        queue.push_back(neighbor);
427
428                        // Add tree edge to sparsifier if not present
429                        if !sparsifier.vertices.contains(&neighbor) {
430                            sparsifier.add_edge(current, neighbor, weight);
431                        }
432                    }
433                }
434            }
435        }
436    }
437
438    /// Compute min-cut on sparsifier
439    fn compute_sparsifier_min_cut(&self, sparsifier: &SpectralSparsifier) -> f64 {
440        if sparsifier.vertex_count() <= 1 {
441            return f64::INFINITY;
442        }
443
444        // Use Stoer-Wagner on the small sparsifier
445        self.stoer_wagner(&sparsifier.adj, &sparsifier.vertices)
446    }
447
448    /// Optimized Stoer-Wagner minimum cut algorithm
449    /// Uses early termination and degree-based lower bound
450    fn stoer_wagner(
451        &self,
452        adj: &HashMap<VertexId, Vec<(VertexId, f64)>>,
453        vertices: &HashSet<VertexId>,
454    ) -> f64 {
455        if vertices.len() <= 1 {
456            return f64::INFINITY;
457        }
458
459        // Quick lower bound: minimum weighted degree
460        let min_degree: f64 = adj
461            .iter()
462            .filter(|(v, _)| vertices.contains(v))
463            .map(|(_, neighbors)| neighbors.iter().map(|(_, w)| *w).sum::<f64>())
464            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
465            .unwrap_or(f64::INFINITY);
466
467        // For very small graphs, use simpler algorithm
468        if vertices.len() <= 3 {
469            return min_degree;
470        }
471
472        // Build adjacency matrix
473        let verts: Vec<VertexId> = vertices.iter().copied().collect();
474        let n = verts.len();
475        let vert_to_idx: HashMap<_, _> = verts.iter().enumerate().map(|(i, &v)| (v, i)).collect();
476
477        let mut weights = vec![vec![0.0; n]; n];
478        for (&v, neighbors) in adj {
479            if let Some(&i) = vert_to_idx.get(&v) {
480                for &(neighbor, weight) in neighbors {
481                    if let Some(&j) = vert_to_idx.get(&neighbor) {
482                        weights[i][j] += weight;
483                    }
484                }
485            }
486        }
487
488        // Stoer-Wagner iterations with optimizations
489        let mut min_cut = f64::INFINITY;
490        let mut active: Vec<bool> = vec![true; n];
491
492        for phase in 0..n - 1 {
493            // Early termination if we found a zero cut
494            if min_cut == 0.0 {
495                break;
496            }
497
498            // Maximum adjacency search with optimized tracking
499            let mut in_a = vec![false; n];
500            let mut cut_of_phase = vec![0.0; n];
501
502            // Find first active vertex
503            let first = match (0..n).find(|&i| active[i]) {
504                Some(f) => f,
505                None => break,
506            };
507            in_a[first] = true;
508
509            let mut last = first;
510            let mut before_last = first;
511
512            let active_count = active.iter().filter(|&&a| a).count();
513            for _ in 1..active_count {
514                // Update cut values only for neighbors of last
515                for j in 0..n {
516                    if active[j] && !in_a[j] && weights[last][j] > 0.0 {
517                        cut_of_phase[j] += weights[last][j];
518                    }
519                }
520
521                // Find vertex with maximum cut value
522                before_last = last;
523                let mut max_val = f64::NEG_INFINITY;
524                for j in 0..n {
525                    if active[j] && !in_a[j] && cut_of_phase[j] > max_val {
526                        max_val = cut_of_phase[j];
527                        last = j;
528                    }
529                }
530
531                if max_val == f64::NEG_INFINITY {
532                    break;
533                }
534                in_a[last] = true;
535            }
536
537            // Update minimum cut
538            if cut_of_phase[last] > 0.0 || phase == 0 {
539                min_cut = min_cut.min(cut_of_phase[last]);
540            }
541
542            // Merge last two vertices
543            active[last] = false;
544            for j in 0..n {
545                weights[before_last][j] += weights[last][j];
546                weights[j][before_last] += weights[j][last];
547            }
548        }
549
550        min_cut
551    }
552
553    /// Compute exact min-cut for small graphs
554    fn compute_exact_min_cut(&self) -> f64 {
555        self.stoer_wagner(&self.adj, &self.vertices)
556    }
557
558    /// Compute partition achieving the approximate min-cut
559    fn compute_partition(&self, _cut_value: f64) -> Option<(Vec<VertexId>, Vec<VertexId>)> {
560        // For now, return a simple partition based on BFS from first vertex
561        if self.vertices.len() <= 1 {
562            return None;
563        }
564
565        let start = *self.vertices.iter().next()?;
566        let mut visited = HashSet::new();
567        let mut queue = VecDeque::new();
568
569        queue.push_back(start);
570        visited.insert(start);
571
572        // Visit half the vertices
573        let target = self.vertices.len() / 2;
574        while visited.len() < target {
575            if let Some(current) = queue.pop_front() {
576                if let Some(neighbors) = self.adj.get(&current) {
577                    for &(neighbor, _) in neighbors {
578                        if !visited.contains(&neighbor) {
579                            visited.insert(neighbor);
580                            queue.push_back(neighbor);
581                            if visited.len() >= target {
582                                break;
583                            }
584                        }
585                    }
586                }
587            } else {
588                break;
589            }
590        }
591
592        let s: Vec<VertexId> = visited.into_iter().collect();
593        let t: Vec<VertexId> = self.vertices.iter()
594            .filter(|v| !s.contains(v))
595            .copied()
596            .collect();
597
598        Some((s, t))
599    }
600}
601
602impl Default for ApproxMinCut {
603    fn default() -> Self {
604        Self::new(ApproxMinCutConfig::default())
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn test_basic_approx_min_cut() {
614        let mut approx = ApproxMinCut::default();
615
616        approx.insert_edge(0, 1, 1.0);
617        approx.insert_edge(1, 2, 1.0);
618        approx.insert_edge(2, 0, 1.0);
619
620        let result = approx.min_cut();
621        assert!(result.value >= 1.8); // Should be close to 2.0
622        assert!(result.value <= 2.2);
623    }
624
625    #[test]
626    fn test_triangle_graph() {
627        let mut approx = ApproxMinCut::with_epsilon(0.1);
628
629        approx.insert_edge(0, 1, 1.0);
630        approx.insert_edge(1, 2, 1.0);
631        approx.insert_edge(2, 0, 1.0);
632
633        let value = approx.min_cut_value();
634        assert!((value - 2.0).abs() < 0.5); // Approximate
635    }
636
637    #[test]
638    fn test_disconnected_graph() {
639        let mut approx = ApproxMinCut::default();
640
641        approx.insert_edge(0, 1, 1.0);
642        approx.insert_edge(2, 3, 1.0);
643
644        assert!(!approx.is_connected());
645        assert_eq!(approx.min_cut_value(), 0.0);
646    }
647
648    #[test]
649    fn test_single_edge() {
650        let mut approx = ApproxMinCut::default();
651
652        approx.insert_edge(0, 1, 5.0);
653
654        let value = approx.min_cut_value();
655        assert!((value - 5.0).abs() < 1.0);
656    }
657
658    #[test]
659    fn test_path_graph() {
660        let mut approx = ApproxMinCut::default();
661
662        // Path: 0 - 1 - 2 - 3
663        approx.insert_edge(0, 1, 1.0);
664        approx.insert_edge(1, 2, 1.0);
665        approx.insert_edge(2, 3, 1.0);
666
667        let value = approx.min_cut_value();
668        assert!(value >= 0.5); // Should be close to 1.0
669        assert!(value <= 1.5);
670    }
671
672    #[test]
673    fn test_delete_edge() {
674        let mut approx = ApproxMinCut::default();
675
676        approx.insert_edge(0, 1, 1.0);
677        approx.insert_edge(1, 2, 1.0);
678        approx.insert_edge(2, 0, 1.0);
679
680        assert!(approx.is_connected());
681
682        approx.delete_edge(1, 2);
683
684        assert!(approx.is_connected());
685        let value = approx.min_cut_value();
686        assert!(value >= 0.5 && value <= 1.5);
687    }
688
689    #[test]
690    fn test_stats() {
691        let mut approx = ApproxMinCut::default();
692
693        approx.insert_edge(0, 1, 1.0);
694        approx.insert_edge(1, 2, 1.0);
695        approx.delete_edge(0, 1);
696        approx.min_cut_value();
697
698        let stats = approx.stats();
699        assert_eq!(stats.insertions, 2);
700        assert_eq!(stats.deletions, 1);
701        assert_eq!(stats.queries, 1);
702    }
703
704    #[test]
705    fn test_result_bounds() {
706        let mut approx = ApproxMinCut::with_epsilon(0.2);
707
708        approx.insert_edge(0, 1, 2.0);
709        approx.insert_edge(1, 2, 2.0);
710        approx.insert_edge(2, 0, 2.0);
711
712        let result = approx.min_cut();
713        assert!(result.lower_bound <= result.value);
714        assert!(result.value <= result.upper_bound);
715        assert_eq!(result.epsilon, 0.2);
716    }
717
718    #[test]
719    fn test_larger_graph() {
720        let mut approx = ApproxMinCut::with_epsilon(0.15);
721
722        // Create a cycle
723        for i in 0..10 {
724            approx.insert_edge(i, (i + 1) % 10, 1.0);
725        }
726
727        let value = approx.min_cut_value();
728        assert!(value >= 1.0); // At least 2.0 theoretically
729        assert!(value <= 3.0);
730    }
731}