ruvector_mincut/fragmentation/
mod.rs

1//! Fragmentation Algorithm for Graph Decomposition
2//!
3//! Implementation of Theorem 5.1 from:
4//! "Deterministic and Exact Fully-dynamic Minimum Cut of Superpolylogarithmic
5//! Size in Subpolynomial Time" (arXiv:2512.13105)
6//!
7//! The Fragmentation algorithm decomposes a graph into expander-like
8//! components with controlled boundary sizes. This enables efficient
9//! maintenance of minimum cuts under dynamic updates.
10//!
11//! # Key Components
12//!
13//! - **Trim subroutine**: Finds boundary-sparse cuts in expanders
14//! - **Recursive fragmentation**: Decomposes graph into hierarchy
15//! - **Expander detection**: Identifies well-connected subgraphs
16
17use crate::graph::{VertexId, Weight};
18use std::collections::{HashMap, HashSet, VecDeque};
19
20/// Configuration for the fragmentation algorithm
21#[derive(Debug, Clone)]
22pub struct FragmentationConfig {
23    /// Expansion parameter φ (phi)
24    pub phi: f64,
25    /// Maximum fragment size before splitting
26    pub max_fragment_size: usize,
27    /// Minimum fragment size (don't split smaller)
28    pub min_fragment_size: usize,
29    /// Boundary sparsity parameter
30    pub boundary_sparsity: f64,
31}
32
33impl Default for FragmentationConfig {
34    fn default() -> Self {
35        Self {
36            phi: 0.1,
37            max_fragment_size: 1000,
38            min_fragment_size: 10,
39            boundary_sparsity: 0.5,
40        }
41    }
42}
43
44/// A fragment (partition) of the graph
45#[derive(Debug, Clone)]
46pub struct Fragment {
47    /// Unique fragment identifier
48    pub id: u64,
49    /// Vertices in this fragment
50    pub vertices: HashSet<VertexId>,
51    /// Boundary edges (crossing to other fragments)
52    pub boundary: Vec<(VertexId, VertexId)>,
53    /// Internal edges (within fragment)
54    pub internal_edge_count: usize,
55    /// Volume (sum of degrees)
56    pub volume: usize,
57    /// Parent fragment ID (if any)
58    pub parent: Option<u64>,
59    /// Child fragment IDs
60    pub children: Vec<u64>,
61    /// Is this fragment an expander?
62    pub is_expander: bool,
63}
64
65impl Fragment {
66    /// Create new fragment
67    pub fn new(id: u64, vertices: HashSet<VertexId>) -> Self {
68        Self {
69            id,
70            vertices,
71            boundary: Vec::new(),
72            internal_edge_count: 0,
73            volume: 0,
74            parent: None,
75            children: Vec::new(),
76            is_expander: false,
77        }
78    }
79
80    /// Get fragment size (number of vertices)
81    pub fn size(&self) -> usize {
82        self.vertices.len()
83    }
84
85    /// Check if vertex is in this fragment
86    pub fn contains(&self, v: VertexId) -> bool {
87        self.vertices.contains(&v)
88    }
89
90    /// Compute boundary sparsity: |boundary| / volume
91    pub fn boundary_sparsity(&self) -> f64 {
92        if self.volume == 0 {
93            return 0.0;
94        }
95        self.boundary.len() as f64 / self.volume as f64
96    }
97}
98
99/// Result of the Trim subroutine
100#[derive(Debug, Clone)]
101pub struct TrimResult {
102    /// Vertices to trim (the sparse boundary region)
103    pub trimmed_vertices: HashSet<VertexId>,
104    /// Edges cut by the trim
105    pub cut_edges: Vec<(VertexId, VertexId)>,
106    /// Cut value (sum of edge weights)
107    pub cut_value: f64,
108    /// Was the trim successful?
109    pub success: bool,
110}
111
112/// The Fragmentation algorithm data structure
113#[derive(Debug)]
114pub struct Fragmentation {
115    /// Configuration
116    config: FragmentationConfig,
117    /// All fragments indexed by ID
118    fragments: HashMap<u64, Fragment>,
119    /// Vertex to fragment mapping
120    vertex_fragment: HashMap<VertexId, u64>,
121    /// Next fragment ID
122    next_id: u64,
123    /// Graph adjacency
124    adjacency: HashMap<VertexId, HashMap<VertexId, Weight>>,
125    /// Root fragment IDs (top-level fragments)
126    roots: Vec<u64>,
127}
128
129impl Fragmentation {
130    /// Create new fragmentation structure
131    pub fn new(config: FragmentationConfig) -> Self {
132        Self {
133            config,
134            fragments: HashMap::new(),
135            vertex_fragment: HashMap::new(),
136            next_id: 1,
137            adjacency: HashMap::new(),
138            roots: Vec::new(),
139        }
140    }
141
142    /// Create with default config
143    pub fn with_defaults() -> Self {
144        Self::new(FragmentationConfig::default())
145    }
146
147    /// Insert an edge into the graph
148    pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) {
149        self.adjacency.entry(u).or_default().insert(v, weight);
150        self.adjacency.entry(v).or_default().insert(u, weight);
151    }
152
153    /// Delete an edge from the graph
154    pub fn delete_edge(&mut self, u: VertexId, v: VertexId) {
155        if let Some(neighbors) = self.adjacency.get_mut(&u) {
156            neighbors.remove(&v);
157        }
158        if let Some(neighbors) = self.adjacency.get_mut(&v) {
159            neighbors.remove(&u);
160        }
161    }
162
163    /// Get all vertices
164    pub fn vertices(&self) -> Vec<VertexId> {
165        self.adjacency.keys().copied().collect()
166    }
167
168    /// Get neighbors of a vertex
169    pub fn neighbors(&self, v: VertexId) -> Vec<(VertexId, Weight)> {
170        self.adjacency
171            .get(&v)
172            .map(|n| n.iter().map(|(&v, &w)| (v, w)).collect())
173            .unwrap_or_default()
174    }
175
176    /// Compute degree of a vertex
177    pub fn degree(&self, v: VertexId) -> usize {
178        self.adjacency.get(&v).map_or(0, |n| n.len())
179    }
180
181    /// Run the fragmentation algorithm to decompose the graph
182    ///
183    /// This implements the recursive decomposition from the paper.
184    pub fn fragment(&mut self) -> Vec<u64> {
185        let vertices: HashSet<_> = self.vertices().into_iter().collect();
186
187        if vertices.is_empty() {
188            return Vec::new();
189        }
190
191        // Create initial fragment with all vertices
192        let root_id = self.create_fragment(vertices);
193        self.roots.push(root_id);
194
195        // Recursively fragment
196        let mut to_process = vec![root_id];
197
198        while let Some(fragment_id) = to_process.pop() {
199            if let Some(children) = self.try_fragment_recursive(fragment_id) {
200                to_process.extend(children);
201            }
202        }
203
204        self.roots.clone()
205    }
206
207    /// Try to recursively fragment a given fragment
208    fn try_fragment_recursive(&mut self, fragment_id: u64) -> Option<Vec<u64>> {
209        let fragment = self.fragments.get(&fragment_id)?;
210
211        // Don't fragment if too small
212        if fragment.size() <= self.config.min_fragment_size {
213            return None;
214        }
215
216        // Don't fragment if already small enough and is an expander
217        if fragment.size() <= self.config.max_fragment_size && fragment.is_expander {
218            return None;
219        }
220
221        // Try to find a sparse cut using Trim
222        let vertices: Vec<_> = fragment.vertices.iter().copied().collect();
223        let trim_result = self.trim(&vertices);
224
225        if !trim_result.success || trim_result.trimmed_vertices.is_empty() {
226            // Mark as expander if we can't find a good cut
227            if let Some(f) = self.fragments.get_mut(&fragment_id) {
228                f.is_expander = true;
229            }
230            return None;
231        }
232
233        // Split into two fragments
234        let remaining: HashSet<_> = fragment
235            .vertices
236            .difference(&trim_result.trimmed_vertices)
237            .copied()
238            .collect();
239
240        if remaining.is_empty() || trim_result.trimmed_vertices.len() == fragment.vertices.len() {
241            return None;
242        }
243
244        // Create child fragments
245        let child1_id = self.create_fragment(trim_result.trimmed_vertices);
246        let child2_id = self.create_fragment(remaining);
247
248        // Update parent-child relationships
249        if let Some(f) = self.fragments.get_mut(&fragment_id) {
250            f.children = vec![child1_id, child2_id];
251        }
252        if let Some(f) = self.fragments.get_mut(&child1_id) {
253            f.parent = Some(fragment_id);
254        }
255        if let Some(f) = self.fragments.get_mut(&child2_id) {
256            f.parent = Some(fragment_id);
257        }
258
259        Some(vec![child1_id, child2_id])
260    }
261
262    /// Create a new fragment from a vertex set
263    fn create_fragment(&mut self, vertices: HashSet<VertexId>) -> u64 {
264        let id = self.next_id;
265        self.next_id += 1;
266
267        // Compute fragment properties
268        let mut internal_edge_count = 0;
269        let mut boundary = Vec::new();
270        let mut volume = 0;
271
272        for &v in &vertices {
273            let neighbors = self.neighbors(v);
274            volume += neighbors.len();
275
276            for (neighbor, _weight) in neighbors {
277                if vertices.contains(&neighbor) {
278                    internal_edge_count += 1;
279                } else {
280                    boundary.push((v, neighbor));
281                }
282            }
283        }
284
285        // Internal edges are counted twice, so divide by 2
286        internal_edge_count /= 2;
287
288        let fragment = Fragment {
289            id,
290            vertices: vertices.clone(),
291            boundary,
292            internal_edge_count,
293            volume,
294            parent: None,
295            children: Vec::new(),
296            is_expander: false,
297        };
298
299        // Update vertex-to-fragment mapping
300        for &v in &vertices {
301            self.vertex_fragment.insert(v, id);
302        }
303
304        self.fragments.insert(id, fragment);
305        id
306    }
307
308    /// Trim subroutine: Find a boundary-sparse cut
309    ///
310    /// Algorithm (per Theorem 5.1):
311    /// 1. Start with high-degree vertices
312    /// 2. Greedily expand the set
313    /// 3. Stop when boundary becomes sparse relative to volume
314    pub fn trim(&self, vertices: &[VertexId]) -> TrimResult {
315        if vertices.is_empty() {
316            return TrimResult {
317                trimmed_vertices: HashSet::new(),
318                cut_edges: Vec::new(),
319                cut_value: 0.0,
320                success: false,
321            };
322        }
323
324        let vertex_set: HashSet<_> = vertices.iter().copied().collect();
325
326        // Start from lowest-degree vertices (more likely to be on boundary)
327        let mut sorted_vertices: Vec<_> = vertices.to_vec();
328        sorted_vertices.sort_by_key(|&v| self.degree(v));
329
330        let mut trimmed = HashSet::new();
331        let mut trimmed_volume = 0usize;
332        let mut boundary_count = 0usize;
333
334        // Greedily add vertices while maintaining sparsity
335        for &v in &sorted_vertices {
336            let neighbors = self.neighbors(v);
337            let degree = neighbors.len();
338
339            // Count how many neighbors are in trimmed vs outside
340            let mut internal_neighbors = 0usize;
341            let mut external_neighbors = 0usize;
342
343            for (neighbor, _) in &neighbors {
344                if trimmed.contains(neighbor) {
345                    internal_neighbors += 1;
346                } else if vertex_set.contains(neighbor) {
347                    // Neighbor in remaining set (will become boundary)
348                    external_neighbors += 1;
349                }
350            }
351
352            // Adding this vertex:
353            // - Removes internal_neighbors from boundary
354            // - Adds external_neighbors to boundary
355            let new_boundary = boundary_count - internal_neighbors + external_neighbors;
356            let new_volume = trimmed_volume + degree;
357
358            // Check sparsity condition
359            let sparsity = if new_volume > 0 {
360                new_boundary as f64 / new_volume as f64
361            } else {
362                0.0
363            };
364
365            if sparsity <= self.config.boundary_sparsity {
366                trimmed.insert(v);
367                trimmed_volume = new_volume;
368                boundary_count = new_boundary;
369            }
370
371            // Stop if we've trimmed enough
372            if trimmed.len() >= vertex_set.len() / 2 {
373                break;
374            }
375        }
376
377        // If we didn't trim anything useful, try a different approach
378        if trimmed.is_empty() || trimmed.len() >= vertex_set.len() {
379            return TrimResult {
380                trimmed_vertices: HashSet::new(),
381                cut_edges: Vec::new(),
382                cut_value: 0.0,
383                success: false,
384            };
385        }
386
387        // Compute cut edges
388        let mut cut_edges = Vec::new();
389        let mut cut_value = 0.0;
390
391        for &v in &trimmed {
392            for (neighbor, weight) in self.neighbors(v) {
393                if !trimmed.contains(&neighbor) && vertex_set.contains(&neighbor) {
394                    cut_edges.push((v, neighbor));
395                    cut_value += weight;
396                }
397            }
398        }
399
400        TrimResult {
401            trimmed_vertices: trimmed,
402            cut_edges,
403            cut_value,
404            success: true,
405        }
406    }
407
408    /// Check if a fragment is a φ-expander
409    ///
410    /// A graph is a φ-expander if every cut (S, S̄) has:
411    /// |∂S| ≥ φ · min(vol(S), vol(S̄))
412    pub fn is_expander(&self, fragment_id: u64) -> bool {
413        let fragment = match self.fragments.get(&fragment_id) {
414            Some(f) => f,
415            None => return false,
416        };
417
418        if fragment.vertices.len() <= 2 {
419            return true; // Trivially an expander
420        }
421
422        // Use the Trim result to check expansion
423        let vertices: Vec<_> = fragment.vertices.iter().copied().collect();
424        let trim = self.trim(&vertices);
425
426        if !trim.success {
427            return true; // No sparse cut found = expander
428        }
429
430        // Check the expansion ratio
431        let cut_volume = trim
432            .trimmed_vertices
433            .iter()
434            .map(|&v| self.degree(v))
435            .sum::<usize>();
436
437        let remaining_volume = fragment.volume - cut_volume;
438        let min_volume = cut_volume.min(remaining_volume);
439
440        if min_volume == 0 {
441            return true;
442        }
443
444        let expansion_ratio = trim.cut_edges.len() as f64 / min_volume as f64;
445        expansion_ratio >= self.config.phi
446    }
447
448    /// Get a fragment by ID
449    pub fn get_fragment(&self, id: u64) -> Option<&Fragment> {
450        self.fragments.get(&id)
451    }
452
453    /// Get fragment containing a vertex
454    pub fn get_vertex_fragment(&self, v: VertexId) -> Option<&Fragment> {
455        self.vertex_fragment
456            .get(&v)
457            .and_then(|&id| self.fragments.get(&id))
458    }
459
460    /// Get all leaf fragments (no children)
461    pub fn leaf_fragments(&self) -> Vec<&Fragment> {
462        self.fragments
463            .values()
464            .filter(|f| f.children.is_empty())
465            .collect()
466    }
467
468    /// Get total number of fragments
469    pub fn num_fragments(&self) -> usize {
470        self.fragments.len()
471    }
472
473    /// Get the fragment hierarchy depth
474    pub fn max_depth(&self) -> usize {
475        fn depth_of(fragments: &HashMap<u64, Fragment>, id: u64) -> usize {
476            match fragments.get(&id) {
477                Some(f) if f.children.is_empty() => 0,
478                Some(f) => {
479                    1 + f
480                        .children
481                        .iter()
482                        .map(|&c| depth_of(fragments, c))
483                        .max()
484                        .unwrap_or(0)
485                }
486                None => 0,
487            }
488        }
489
490        self.roots
491            .iter()
492            .map(|&r| depth_of(&self.fragments, r))
493            .max()
494            .unwrap_or(0)
495    }
496}
497
498impl Default for Fragmentation {
499    fn default() -> Self {
500        Self::with_defaults()
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    fn build_path_graph(frag: &mut Fragmentation, n: usize) {
509        for i in 0..n - 1 {
510            frag.insert_edge(i as u64, (i + 1) as u64, 1.0);
511        }
512    }
513
514    fn build_clique(frag: &mut Fragmentation, vertices: &[u64]) {
515        for i in 0..vertices.len() {
516            for j in i + 1..vertices.len() {
517                frag.insert_edge(vertices[i], vertices[j], 1.0);
518            }
519        }
520    }
521
522    #[test]
523    fn test_fragmentation_empty() {
524        let mut frag = Fragmentation::with_defaults();
525        let roots = frag.fragment();
526        assert!(roots.is_empty());
527    }
528
529    #[test]
530    fn test_fragmentation_single_vertex() {
531        let mut frag = Fragmentation::with_defaults();
532        frag.insert_edge(1, 1, 0.0); // Self-loop to register vertex
533        frag.adjacency.entry(1).or_default(); // Actually just add the vertex
534
535        // For single vertex, we need a proper setup
536        let mut frag2 = Fragmentation::with_defaults();
537        frag2.insert_edge(1, 2, 1.0);
538        let roots = frag2.fragment();
539        assert_eq!(roots.len(), 1);
540    }
541
542    #[test]
543    fn test_fragmentation_path() {
544        let mut frag = Fragmentation::new(FragmentationConfig {
545            min_fragment_size: 2,
546            max_fragment_size: 5,
547            ..Default::default()
548        });
549
550        build_path_graph(&mut frag, 10);
551        let roots = frag.fragment();
552
553        assert!(!roots.is_empty());
554        assert!(frag.num_fragments() >= 1);
555    }
556
557    #[test]
558    fn test_fragmentation_clique() {
559        let mut frag = Fragmentation::with_defaults();
560        let vertices: Vec<u64> = (1..=6).collect();
561        build_clique(&mut frag, &vertices);
562
563        let roots = frag.fragment();
564
565        assert!(!roots.is_empty());
566        // A small clique should be a single expander
567        let leaves = frag.leaf_fragments();
568        let leaf = leaves.first().unwrap();
569        assert!(leaf.size() <= 6);
570    }
571
572    #[test]
573    fn test_trim_basic() {
574        let mut frag = Fragmentation::with_defaults();
575        build_path_graph(&mut frag, 10);
576
577        let vertices: Vec<u64> = (0..10).collect();
578        let result = frag.trim(&vertices);
579
580        // Trim might find a cut or not depending on sparsity params
581        // Just verify it doesn't crash and returns valid result
582        assert!(result.trimmed_vertices.len() <= vertices.len());
583    }
584
585    #[test]
586    fn test_fragment_properties() {
587        let mut frag = Fragmentation::with_defaults();
588
589        // Two cliques connected by a single edge
590        build_clique(&mut frag, &[1, 2, 3, 4]);
591        build_clique(&mut frag, &[5, 6, 7, 8]);
592        frag.insert_edge(4, 5, 1.0); // Bridge edge
593
594        let roots = frag.fragment();
595
596        // Should fragment at the bridge
597        assert!(!roots.is_empty());
598
599        // Check we can get vertex fragments
600        let f1 = frag.get_vertex_fragment(1);
601        assert!(f1.is_some());
602    }
603
604    #[test]
605    fn test_is_expander() {
606        let mut frag = Fragmentation::new(FragmentationConfig {
607            phi: 0.1,
608            min_fragment_size: 2,
609            ..Default::default()
610        });
611
612        // Build a clique (should be an expander)
613        build_clique(&mut frag, &[1, 2, 3, 4, 5]);
614
615        let roots = frag.fragment();
616        assert!(!roots.is_empty());
617
618        // The fragment should be an expander (dense graph)
619        let is_exp = frag.is_expander(roots[0]);
620        // Cliques are typically expanders
621        assert!(is_exp || frag.leaf_fragments().len() > 1);
622    }
623
624    #[test]
625    fn test_hierarchy_depth() {
626        let mut frag = Fragmentation::new(FragmentationConfig {
627            min_fragment_size: 3,
628            max_fragment_size: 10,
629            ..Default::default()
630        });
631
632        build_path_graph(&mut frag, 20);
633        frag.fragment();
634
635        let depth = frag.max_depth();
636        // Path graph might get split a few times
637        assert!(depth >= 0);
638    }
639}