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 std::collections::{HashMap, HashSet, VecDeque};
18use crate::graph::{VertexId, Weight};
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.get(&v)
171            .map(|n| n.iter().map(|(&v, &w)| (v, w)).collect())
172            .unwrap_or_default()
173    }
174
175    /// Compute degree of a vertex
176    pub fn degree(&self, v: VertexId) -> usize {
177        self.adjacency.get(&v).map_or(0, |n| n.len())
178    }
179
180    /// Run the fragmentation algorithm to decompose the graph
181    ///
182    /// This implements the recursive decomposition from the paper.
183    pub fn fragment(&mut self) -> Vec<u64> {
184        let vertices: HashSet<_> = self.vertices().into_iter().collect();
185
186        if vertices.is_empty() {
187            return Vec::new();
188        }
189
190        // Create initial fragment with all vertices
191        let root_id = self.create_fragment(vertices);
192        self.roots.push(root_id);
193
194        // Recursively fragment
195        let mut to_process = vec![root_id];
196
197        while let Some(fragment_id) = to_process.pop() {
198            if let Some(children) = self.try_fragment_recursive(fragment_id) {
199                to_process.extend(children);
200            }
201        }
202
203        self.roots.clone()
204    }
205
206    /// Try to recursively fragment a given fragment
207    fn try_fragment_recursive(&mut self, fragment_id: u64) -> Option<Vec<u64>> {
208        let fragment = self.fragments.get(&fragment_id)?;
209
210        // Don't fragment if too small
211        if fragment.size() <= self.config.min_fragment_size {
212            return None;
213        }
214
215        // Don't fragment if already small enough and is an expander
216        if fragment.size() <= self.config.max_fragment_size && fragment.is_expander {
217            return None;
218        }
219
220        // Try to find a sparse cut using Trim
221        let vertices: Vec<_> = fragment.vertices.iter().copied().collect();
222        let trim_result = self.trim(&vertices);
223
224        if !trim_result.success || trim_result.trimmed_vertices.is_empty() {
225            // Mark as expander if we can't find a good cut
226            if let Some(f) = self.fragments.get_mut(&fragment_id) {
227                f.is_expander = true;
228            }
229            return None;
230        }
231
232        // Split into two fragments
233        let remaining: HashSet<_> = fragment.vertices
234            .difference(&trim_result.trimmed_vertices)
235            .copied()
236            .collect();
237
238        if remaining.is_empty() || trim_result.trimmed_vertices.len() == fragment.vertices.len() {
239            return None;
240        }
241
242        // Create child fragments
243        let child1_id = self.create_fragment(trim_result.trimmed_vertices);
244        let child2_id = self.create_fragment(remaining);
245
246        // Update parent-child relationships
247        if let Some(f) = self.fragments.get_mut(&fragment_id) {
248            f.children = vec![child1_id, child2_id];
249        }
250        if let Some(f) = self.fragments.get_mut(&child1_id) {
251            f.parent = Some(fragment_id);
252        }
253        if let Some(f) = self.fragments.get_mut(&child2_id) {
254            f.parent = Some(fragment_id);
255        }
256
257        Some(vec![child1_id, child2_id])
258    }
259
260    /// Create a new fragment from a vertex set
261    fn create_fragment(&mut self, vertices: HashSet<VertexId>) -> u64 {
262        let id = self.next_id;
263        self.next_id += 1;
264
265        // Compute fragment properties
266        let mut internal_edge_count = 0;
267        let mut boundary = Vec::new();
268        let mut volume = 0;
269
270        for &v in &vertices {
271            let neighbors = self.neighbors(v);
272            volume += neighbors.len();
273
274            for (neighbor, _weight) in neighbors {
275                if vertices.contains(&neighbor) {
276                    internal_edge_count += 1;
277                } else {
278                    boundary.push((v, neighbor));
279                }
280            }
281        }
282
283        // Internal edges are counted twice, so divide by 2
284        internal_edge_count /= 2;
285
286        let fragment = Fragment {
287            id,
288            vertices: vertices.clone(),
289            boundary,
290            internal_edge_count,
291            volume,
292            parent: None,
293            children: Vec::new(),
294            is_expander: false,
295        };
296
297        // Update vertex-to-fragment mapping
298        for &v in &vertices {
299            self.vertex_fragment.insert(v, id);
300        }
301
302        self.fragments.insert(id, fragment);
303        id
304    }
305
306    /// Trim subroutine: Find a boundary-sparse cut
307    ///
308    /// Algorithm (per Theorem 5.1):
309    /// 1. Start with high-degree vertices
310    /// 2. Greedily expand the set
311    /// 3. Stop when boundary becomes sparse relative to volume
312    pub fn trim(&self, vertices: &[VertexId]) -> TrimResult {
313        if vertices.is_empty() {
314            return TrimResult {
315                trimmed_vertices: HashSet::new(),
316                cut_edges: Vec::new(),
317                cut_value: 0.0,
318                success: false,
319            };
320        }
321
322        let vertex_set: HashSet<_> = vertices.iter().copied().collect();
323
324        // Start from lowest-degree vertices (more likely to be on boundary)
325        let mut sorted_vertices: Vec<_> = vertices.to_vec();
326        sorted_vertices.sort_by_key(|&v| self.degree(v));
327
328        let mut trimmed = HashSet::new();
329        let mut trimmed_volume = 0usize;
330        let mut boundary_count = 0usize;
331
332        // Greedily add vertices while maintaining sparsity
333        for &v in &sorted_vertices {
334            let neighbors = self.neighbors(v);
335            let degree = neighbors.len();
336
337            // Count how many neighbors are in trimmed vs outside
338            let mut internal_neighbors = 0usize;
339            let mut external_neighbors = 0usize;
340
341            for (neighbor, _) in &neighbors {
342                if trimmed.contains(neighbor) {
343                    internal_neighbors += 1;
344                } else if vertex_set.contains(neighbor) {
345                    // Neighbor in remaining set (will become boundary)
346                    external_neighbors += 1;
347                }
348            }
349
350            // Adding this vertex:
351            // - Removes internal_neighbors from boundary
352            // - Adds external_neighbors to boundary
353            let new_boundary = boundary_count - internal_neighbors + external_neighbors;
354            let new_volume = trimmed_volume + degree;
355
356            // Check sparsity condition
357            let sparsity = if new_volume > 0 {
358                new_boundary as f64 / new_volume as f64
359            } else {
360                0.0
361            };
362
363            if sparsity <= self.config.boundary_sparsity {
364                trimmed.insert(v);
365                trimmed_volume = new_volume;
366                boundary_count = new_boundary;
367            }
368
369            // Stop if we've trimmed enough
370            if trimmed.len() >= vertex_set.len() / 2 {
371                break;
372            }
373        }
374
375        // If we didn't trim anything useful, try a different approach
376        if trimmed.is_empty() || trimmed.len() >= vertex_set.len() {
377            return TrimResult {
378                trimmed_vertices: HashSet::new(),
379                cut_edges: Vec::new(),
380                cut_value: 0.0,
381                success: false,
382            };
383        }
384
385        // Compute cut edges
386        let mut cut_edges = Vec::new();
387        let mut cut_value = 0.0;
388
389        for &v in &trimmed {
390            for (neighbor, weight) in self.neighbors(v) {
391                if !trimmed.contains(&neighbor) && vertex_set.contains(&neighbor) {
392                    cut_edges.push((v, neighbor));
393                    cut_value += weight;
394                }
395            }
396        }
397
398        TrimResult {
399            trimmed_vertices: trimmed,
400            cut_edges,
401            cut_value,
402            success: true,
403        }
404    }
405
406    /// Check if a fragment is a φ-expander
407    ///
408    /// A graph is a φ-expander if every cut (S, S̄) has:
409    /// |∂S| ≥ φ · min(vol(S), vol(S̄))
410    pub fn is_expander(&self, fragment_id: u64) -> bool {
411        let fragment = match self.fragments.get(&fragment_id) {
412            Some(f) => f,
413            None => return false,
414        };
415
416        if fragment.vertices.len() <= 2 {
417            return true; // Trivially an expander
418        }
419
420        // Use the Trim result to check expansion
421        let vertices: Vec<_> = fragment.vertices.iter().copied().collect();
422        let trim = self.trim(&vertices);
423
424        if !trim.success {
425            return true; // No sparse cut found = expander
426        }
427
428        // Check the expansion ratio
429        let cut_volume = trim.trimmed_vertices.iter()
430            .map(|&v| self.degree(v))
431            .sum::<usize>();
432
433        let remaining_volume = fragment.volume - cut_volume;
434        let min_volume = cut_volume.min(remaining_volume);
435
436        if min_volume == 0 {
437            return true;
438        }
439
440        let expansion_ratio = trim.cut_edges.len() as f64 / min_volume as f64;
441        expansion_ratio >= self.config.phi
442    }
443
444    /// Get a fragment by ID
445    pub fn get_fragment(&self, id: u64) -> Option<&Fragment> {
446        self.fragments.get(&id)
447    }
448
449    /// Get fragment containing a vertex
450    pub fn get_vertex_fragment(&self, v: VertexId) -> Option<&Fragment> {
451        self.vertex_fragment.get(&v)
452            .and_then(|&id| self.fragments.get(&id))
453    }
454
455    /// Get all leaf fragments (no children)
456    pub fn leaf_fragments(&self) -> Vec<&Fragment> {
457        self.fragments.values()
458            .filter(|f| f.children.is_empty())
459            .collect()
460    }
461
462    /// Get total number of fragments
463    pub fn num_fragments(&self) -> usize {
464        self.fragments.len()
465    }
466
467    /// Get the fragment hierarchy depth
468    pub fn max_depth(&self) -> usize {
469        fn depth_of(fragments: &HashMap<u64, Fragment>, id: u64) -> usize {
470            match fragments.get(&id) {
471                Some(f) if f.children.is_empty() => 0,
472                Some(f) => 1 + f.children.iter()
473                    .map(|&c| depth_of(fragments, c))
474                    .max()
475                    .unwrap_or(0),
476                None => 0,
477            }
478        }
479
480        self.roots.iter()
481            .map(|&r| depth_of(&self.fragments, r))
482            .max()
483            .unwrap_or(0)
484    }
485}
486
487impl Default for Fragmentation {
488    fn default() -> Self {
489        Self::with_defaults()
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    fn build_path_graph(frag: &mut Fragmentation, n: usize) {
498        for i in 0..n-1 {
499            frag.insert_edge(i as u64, (i + 1) as u64, 1.0);
500        }
501    }
502
503    fn build_clique(frag: &mut Fragmentation, vertices: &[u64]) {
504        for i in 0..vertices.len() {
505            for j in i+1..vertices.len() {
506                frag.insert_edge(vertices[i], vertices[j], 1.0);
507            }
508        }
509    }
510
511    #[test]
512    fn test_fragmentation_empty() {
513        let mut frag = Fragmentation::with_defaults();
514        let roots = frag.fragment();
515        assert!(roots.is_empty());
516    }
517
518    #[test]
519    fn test_fragmentation_single_vertex() {
520        let mut frag = Fragmentation::with_defaults();
521        frag.insert_edge(1, 1, 0.0); // Self-loop to register vertex
522        frag.adjacency.entry(1).or_default(); // Actually just add the vertex
523
524        // For single vertex, we need a proper setup
525        let mut frag2 = Fragmentation::with_defaults();
526        frag2.insert_edge(1, 2, 1.0);
527        let roots = frag2.fragment();
528        assert_eq!(roots.len(), 1);
529    }
530
531    #[test]
532    fn test_fragmentation_path() {
533        let mut frag = Fragmentation::new(FragmentationConfig {
534            min_fragment_size: 2,
535            max_fragment_size: 5,
536            ..Default::default()
537        });
538
539        build_path_graph(&mut frag, 10);
540        let roots = frag.fragment();
541
542        assert!(!roots.is_empty());
543        assert!(frag.num_fragments() >= 1);
544    }
545
546    #[test]
547    fn test_fragmentation_clique() {
548        let mut frag = Fragmentation::with_defaults();
549        let vertices: Vec<u64> = (1..=6).collect();
550        build_clique(&mut frag, &vertices);
551
552        let roots = frag.fragment();
553
554        assert!(!roots.is_empty());
555        // A small clique should be a single expander
556        let leaves = frag.leaf_fragments();
557        let leaf = leaves.first().unwrap();
558        assert!(leaf.size() <= 6);
559    }
560
561    #[test]
562    fn test_trim_basic() {
563        let mut frag = Fragmentation::with_defaults();
564        build_path_graph(&mut frag, 10);
565
566        let vertices: Vec<u64> = (0..10).collect();
567        let result = frag.trim(&vertices);
568
569        // Trim might find a cut or not depending on sparsity params
570        // Just verify it doesn't crash and returns valid result
571        assert!(result.trimmed_vertices.len() <= vertices.len());
572    }
573
574    #[test]
575    fn test_fragment_properties() {
576        let mut frag = Fragmentation::with_defaults();
577
578        // Two cliques connected by a single edge
579        build_clique(&mut frag, &[1, 2, 3, 4]);
580        build_clique(&mut frag, &[5, 6, 7, 8]);
581        frag.insert_edge(4, 5, 1.0); // Bridge edge
582
583        let roots = frag.fragment();
584
585        // Should fragment at the bridge
586        assert!(!roots.is_empty());
587
588        // Check we can get vertex fragments
589        let f1 = frag.get_vertex_fragment(1);
590        assert!(f1.is_some());
591    }
592
593    #[test]
594    fn test_is_expander() {
595        let mut frag = Fragmentation::new(FragmentationConfig {
596            phi: 0.1,
597            min_fragment_size: 2,
598            ..Default::default()
599        });
600
601        // Build a clique (should be an expander)
602        build_clique(&mut frag, &[1, 2, 3, 4, 5]);
603
604        let roots = frag.fragment();
605        assert!(!roots.is_empty());
606
607        // The fragment should be an expander (dense graph)
608        let is_exp = frag.is_expander(roots[0]);
609        // Cliques are typically expanders
610        assert!(is_exp || frag.leaf_fragments().len() > 1);
611    }
612
613    #[test]
614    fn test_hierarchy_depth() {
615        let mut frag = Fragmentation::new(FragmentationConfig {
616            min_fragment_size: 3,
617            max_fragment_size: 10,
618            ..Default::default()
619        });
620
621        build_path_graph(&mut frag, 20);
622        frag.fragment();
623
624        let depth = frag.max_depth();
625        // Path graph might get split a few times
626        assert!(depth >= 0);
627    }
628}