scirs2_graph/algorithms/
hypergraph.rs

1//! Hypergraph algorithms
2//!
3//! This module contains algorithms specifically designed for hypergraphs,
4//! including transversals, cuts, and hypergraph-specific analysis methods.
5
6use crate::base::{EdgeWeight, Hypergraph, IndexType, Node};
7use crate::error::{GraphError, Result};
8use std::collections::{HashMap, HashSet, VecDeque};
9
10/// Result of hypergraph cut analysis
11#[derive(Debug, Clone)]
12pub struct HypergraphCut<N: Node> {
13    /// Nodes in the first partition
14    pub partition_a: HashSet<N>,
15    /// Nodes in the second partition  
16    pub partition_b: HashSet<N>,
17    /// Hyperedges that cross the cut (connect both partitions)
18    pub cut_hyperedges: Vec<usize>,
19    /// Total weight of cut hyperedges
20    pub cut_weight: f64,
21}
22
23/// Represents a minimal transversal (hitting set) of a hypergraph
24#[derive(Debug, Clone)]
25pub struct MinimalTransversal<N: Node> {
26    /// Nodes in the transversal
27    pub nodes: HashSet<N>,
28    /// Hyperedges hit by this transversal
29    pub hit_hyperedges: Vec<usize>,
30    /// Size of the transversal
31    pub size: usize,
32}
33
34/// Find all minimal transversals (hitting sets) of a hypergraph
35///
36/// A transversal is a set of nodes that intersects every hyperedge.
37/// This is also known as the hitting set problem.
38///
39/// # Arguments
40/// * `hypergraph` - The hypergraph to analyze
41/// * `max_size` - Maximum size of transversals to consider (for efficiency)
42///
43/// # Returns
44/// * Vector of minimal transversals
45#[allow(dead_code)]
46pub fn minimal_transversals<N, E, Ix>(
47    hypergraph: &Hypergraph<N, E, Ix>,
48    max_size: Option<usize>,
49) -> Vec<MinimalTransversal<N>>
50where
51    N: Node + Clone + Ord + std::fmt::Debug,
52    E: EdgeWeight,
53    Ix: IndexType,
54{
55    let mut transversals = Vec::new();
56    let nodes: Vec<N> = hypergraph.nodes().cloned().collect();
57    let hyperedges = hypergraph.hyperedges();
58
59    if nodes.is_empty() || hyperedges.is_empty() {
60        return transversals;
61    }
62
63    let max_size = max_size.unwrap_or(nodes.len());
64
65    // Try all possible subsets up to max_size
66    for size in 1..=max_size.min(nodes.len()) {
67        let combinations = generate_combinations(&nodes, size);
68
69        for candidate in combinations {
70            let candidate_set: HashSet<N> = candidate.into_iter().collect();
71
72            // Check if this candidate hits all hyperedges
73            let mut hit_hyperedges = Vec::new();
74            let mut hits_all = true;
75
76            for hyperedge in hyperedges {
77                let hyperedge_nodes: std::collections::HashSet<N> =
78                    hyperedge.nodes.iter().cloned().collect();
79                if candidate_set
80                    .intersection(&hyperedge_nodes)
81                    .next()
82                    .is_some()
83                {
84                    hit_hyperedges.push(hyperedge.id);
85                } else {
86                    hits_all = false;
87                    break;
88                }
89            }
90
91            if hits_all {
92                // Check if this is minimal (no proper subset is also a transversal)
93                let mut is_minimal = true;
94                for existing in &transversals {
95                    if existing.nodes.is_subset(&candidate_set) && existing.nodes != candidate_set {
96                        is_minimal = false;
97                        break;
98                    }
99                }
100
101                if is_minimal {
102                    // Remove any existing transversals that are supersets of this one
103                    transversals
104                        .retain(|t| !candidate_set.is_subset(&t.nodes) || t.nodes == candidate_set);
105
106                    transversals.push(MinimalTransversal {
107                        size: candidate_set.len(),
108                        nodes: candidate_set,
109                        hit_hyperedges,
110                    });
111                }
112            }
113        }
114    }
115
116    transversals
117}
118
119/// Generate all combinations of k elements from a vector
120#[allow(dead_code)]
121fn generate_combinations<T: Clone>(items: &[T], k: usize) -> Vec<Vec<T>> {
122    if k == 0 {
123        return vec![vec![]];
124    }
125    if k > items.len() {
126        return vec![];
127    }
128    if k == items.len() {
129        return vec![items.to_vec()];
130    }
131
132    let mut result = Vec::new();
133
134    // Include first element
135    let first = items[0].clone();
136    let rest = &items[1..];
137    for mut combo in generate_combinations(rest, k - 1) {
138        combo.insert(0, first.clone());
139        result.push(combo);
140    }
141
142    // Exclude first element
143    result.extend(generate_combinations(rest, k));
144
145    result
146}
147
148/// Find the minimum vertex cut in a hypergraph
149///
150/// Converts the hypergraph to its 2-section graph and finds minimum vertex cut.
151///
152/// # Arguments
153/// * `hypergraph` - The hypergraph to analyze
154/// * `source` - Source node for cut analysis
155/// * `target` - Target node for cut analysis
156///
157/// # Returns
158/// * The minimum cut information
159#[allow(dead_code)]
160pub fn minimum_vertex_cut<N, E, Ix>(
161    hypergraph: &Hypergraph<N, E, Ix>,
162    source: &N,
163    target: &N,
164) -> Result<HypergraphCut<N>>
165where
166    N: Node + Clone + Ord + std::fmt::Debug,
167    E: EdgeWeight + Clone + Default + num_traits::Zero + std::ops::Add<Output = E> + Into<f64>,
168    Ix: IndexType,
169{
170    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
171        return Err(GraphError::node_not_found("node"));
172    }
173
174    if source == target {
175        return Err(GraphError::InvalidGraph(
176            "Source and target cannot be the same".to_string(),
177        ));
178    }
179
180    // For now, implement a simple approach by converting to 2-section
181    // and finding a basic cut using BFS-based approach
182    let _graph = hypergraph.to_graph();
183
184    // Simple approach: partition nodes based on distance from source
185    // In a proper implementation, this would use max-flow min-cut
186    let all_nodes: HashSet<N> = hypergraph.nodes().cloned().collect();
187
188    // For now, create a simple partition by splitting nodes
189    let mut partition_a = HashSet::new();
190    let mut partition_b = HashSet::new();
191
192    partition_a.insert(source.clone());
193    partition_b.insert(target.clone());
194
195    // Add remaining nodes to balance partitions
196    for node in &all_nodes {
197        if node != source && node != target {
198            if partition_a.len() <= partition_b.len() {
199                partition_a.insert(node.clone());
200            } else {
201                partition_b.insert(node.clone());
202            }
203        }
204    }
205
206    // Find hyperedges that cross the cut
207    let mut cut_hyperedges = Vec::new();
208    let mut cut_weight = 0.0;
209
210    for hyperedge in hypergraph.hyperedges() {
211        let hyperedge_nodes: std::collections::HashSet<N> =
212            hyperedge.nodes.iter().cloned().collect();
213        let has_a = hyperedge_nodes.intersection(&partition_a).next().is_some();
214        let has_b = hyperedge_nodes.intersection(&partition_b).next().is_some();
215
216        if has_a && has_b {
217            cut_hyperedges.push(hyperedge.id);
218            cut_weight += hyperedge.weight.clone().into();
219        }
220    }
221
222    Ok(HypergraphCut {
223        partition_a,
224        partition_b,
225        cut_hyperedges,
226        cut_weight,
227    })
228}
229
230/// Compute hypergraph connectivity between two nodes
231///
232/// Returns the minimum number of hyperedges that need to be removed
233/// to disconnect two nodes.
234///
235/// # Arguments
236/// * `hypergraph` - The hypergraph to analyze
237/// * `source` - First node
238/// * `target` - Second node
239///
240/// # Returns
241/// * The hyperedge connectivity (minimum number of hyperedges to remove)
242#[allow(dead_code)]
243pub fn hyperedge_connectivity<N, E, Ix>(
244    hypergraph: &Hypergraph<N, E, Ix>,
245    source: &N,
246    target: &N,
247) -> Result<usize>
248where
249    N: Node + Clone + Ord + std::fmt::Debug,
250    E: EdgeWeight + Clone,
251    Ix: IndexType,
252{
253    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
254        return Err(GraphError::node_not_found("node"));
255    }
256
257    if source == target {
258        return Ok(0);
259    }
260
261    // First check if nodes are actually connected
262    if !hypergraph.are_connected(source, target) {
263        return Ok(0);
264    }
265
266    // Find all hyperedges and try removing minimum number to disconnect nodes
267    let hyperedges = hypergraph.hyperedges();
268    let connecting_hyperedges = hypergraph.connecting_hyperedges(source, target);
269
270    // If they share direct hyperedges, need at least 1 removal
271    if !connecting_hyperedges.is_empty() {
272        return Ok(1);
273    }
274
275    // Use a more sophisticated approach for indirect connections
276    // Try removing each hyperedge individually and check connectivity
277    for hyperedge in hyperedges {
278        let mut temphypergraph: Hypergraph<N, E, Ix> = Hypergraph::new();
279
280        // Add all nodes
281        for node in hypergraph.nodes() {
282            temphypergraph.add_node(node.clone());
283        }
284
285        // Add all hyperedges except the one we're testing
286        for other_hyperedge in hyperedges {
287            if other_hyperedge.id != hyperedge.id {
288                temphypergraph.add_hyperedge(
289                    other_hyperedge.nodes.clone(),
290                    other_hyperedge.weight.clone(),
291                )?;
292            }
293        }
294
295        // Check if nodes are still connected
296        if !temphypergraph.are_connected(source, target) {
297            return Ok(1);
298        }
299    }
300
301    // If single hyperedge removal doesn't disconnect, try pairs
302    // This is a simplified approach - full implementation would use max-flow
303    Ok(2)
304}
305
306/// Compute the hypergraph diameter
307///
308/// The diameter is the maximum distance between any pair of connected nodes,
309/// where distance is the minimum number of hyperedges in a path.
310///
311/// # Arguments
312/// * `hypergraph` - The hypergraph to analyze
313///
314/// # Returns
315/// * The diameter, or None if the hypergraph is disconnected
316#[allow(dead_code)]
317pub fn hypergraph_diameter<N, E, Ix>(hypergraph: &Hypergraph<N, E, Ix>) -> Option<usize>
318where
319    N: Node + Clone + Ord + std::fmt::Debug,
320    E: EdgeWeight,
321    Ix: IndexType,
322{
323    let nodes: Vec<N> = hypergraph.nodes().cloned().collect();
324    if nodes.len() < 2 {
325        return Some(0);
326    }
327
328    let mut max_distance = 0;
329
330    for i in 0..nodes.len() {
331        for j in (i + 1)..nodes.len() {
332            if let Some(distance) = hypergraph_distance(hypergraph, &nodes[i], &nodes[j]) {
333                max_distance = max_distance.max(distance);
334            } else {
335                // Graph is disconnected
336                return None;
337            }
338        }
339    }
340
341    Some(max_distance)
342}
343
344/// Compute the distance between two nodes in a hypergraph
345///
346/// Distance is the minimum number of hyperedges needed to connect two nodes.
347///
348/// # Arguments
349/// * `hypergraph` - The hypergraph to analyze
350/// * `source` - Source node
351/// * `target` - Target node
352///
353/// # Returns
354/// * The distance, or None if nodes are not connected
355#[allow(dead_code)]
356pub fn hypergraph_distance<N, E, Ix>(
357    hypergraph: &Hypergraph<N, E, Ix>,
358    source: &N,
359    target: &N,
360) -> Option<usize>
361where
362    N: Node + Clone + Ord + std::fmt::Debug,
363    E: EdgeWeight,
364    Ix: IndexType,
365{
366    if source == target {
367        return Some(0);
368    }
369
370    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
371        return None;
372    }
373
374    // BFS to find shortest path in terms of hyperedges
375    let mut visited = HashSet::new();
376    let mut queue = VecDeque::new();
377    let mut distances = HashMap::new();
378
379    queue.push_back(source.clone());
380    visited.insert(source.clone());
381    distances.insert(source.clone(), 0);
382
383    while let Some(current) = queue.pop_front() {
384        let current_distance = distances[&current];
385
386        // Get all hyperedges containing current node
387        for hyperedge in hypergraph.hyperedges_containing_node(&current) {
388            // Visit all other nodes in this hyperedge
389            for neighbor in &hyperedge.nodes {
390                if neighbor != &current && !visited.contains(neighbor) {
391                    visited.insert(neighbor.clone());
392                    distances.insert(neighbor.clone(), current_distance + 1);
393                    queue.push_back(neighbor.clone());
394
395                    if neighbor == target {
396                        return Some(current_distance + 1);
397                    }
398                }
399            }
400        }
401    }
402
403    None
404}
405
406/// Find strongly connected components in a directed hypergraph
407///
408/// This is an extension of the concept to hypergraphs by treating
409/// hyperedges as directed from one subset of nodes to another.
410/// For simplicity, this implementation treats hyperedges as connecting
411/// all nodes bidirectionally.
412///
413/// # Arguments
414/// * `hypergraph` - The hypergraph to analyze
415///
416/// # Returns
417/// * Vector of strongly connected components (as sets of nodes)
418#[allow(dead_code)]
419pub fn hypergraph_connected_components<N, E, Ix>(
420    hypergraph: &Hypergraph<N, E, Ix>,
421) -> Vec<HashSet<N>>
422where
423    N: Node + Clone + Ord + std::fmt::Debug,
424    E: EdgeWeight,
425    Ix: IndexType,
426{
427    let mut components = Vec::new();
428    let mut visited: HashSet<N> = HashSet::new();
429
430    for node in hypergraph.nodes() {
431        if !visited.contains(node) {
432            let mut component = HashSet::new();
433            let mut stack = vec![node.clone()];
434
435            while let Some(current) = stack.pop() {
436                if !visited.contains(&current) {
437                    visited.insert(current.clone());
438                    component.insert(current.clone());
439
440                    // Add all neighbors through hyperedges
441                    let neighbors = hypergraph.neighbors(&current);
442                    for neighbor in neighbors {
443                        if !visited.contains(&neighbor) {
444                            stack.push(neighbor);
445                        }
446                    }
447                }
448            }
449
450            if !component.is_empty() {
451                components.push(component);
452            }
453        }
454    }
455
456    components
457}
458
459/// Check if a hypergraph is connected
460///
461/// A hypergraph is connected if there is a path between every pair of nodes.
462///
463/// # Arguments
464/// * `hypergraph` - The hypergraph to check
465///
466/// # Returns
467/// * True if connected, false otherwise
468#[allow(dead_code)]
469pub fn ishypergraph_connected<N, E, Ix>(hypergraph: &Hypergraph<N, E, Ix>) -> bool
470where
471    N: Node + Clone + Ord + std::fmt::Debug,
472    E: EdgeWeight,
473    Ix: IndexType,
474{
475    let components = hypergraph_connected_components(hypergraph);
476    components.len() <= 1
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::base::Hypergraph;
483
484    #[test]
485    fn test_minimal_transversals_simple() {
486        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
487
488        // Add hyperedges: {1,2}, {2,3}, {1,3}
489        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
490        hypergraph.add_hyperedge_from_vec(vec![2, 3], 1.0).unwrap();
491        hypergraph.add_hyperedge_from_vec(vec![1, 3], 1.0).unwrap();
492
493        let transversals = minimal_transversals(&hypergraph, Some(2));
494
495        // Should find minimal transversals
496        assert!(!transversals.is_empty());
497
498        // Each transversal should hit all hyperedges
499        for transversal in &transversals {
500            assert_eq!(transversal.hit_hyperedges.len(), 3);
501        }
502    }
503
504    #[test]
505    fn testhypergraph_distance() {
506        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
507
508        // Create a path-like structure: {A,B}, {B,C}, {C,D}
509        hypergraph
510            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
511            .unwrap();
512        hypergraph
513            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
514            .unwrap();
515        hypergraph
516            .add_hyperedge_from_vec(vec!["C", "D"], 1.0)
517            .unwrap();
518
519        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"A"), Some(0));
520        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"B"), Some(1));
521        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"C"), Some(2));
522        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"D"), Some(3));
523
524        // Test disconnected nodes
525        hypergraph.add_node("E");
526        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"E"), None);
527    }
528
529    #[test]
530    fn testhypergraph_diameter() {
531        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
532
533        // Create a path: {1,2}, {2,3}, {3,4}
534        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
535        hypergraph.add_hyperedge_from_vec(vec![2, 3], 1.0).unwrap();
536        hypergraph.add_hyperedge_from_vec(vec![3, 4], 1.0).unwrap();
537
538        assert_eq!(hypergraph_diameter(&hypergraph), Some(3));
539
540        // Add a shortcut
541        hypergraph.add_hyperedge_from_vec(vec![1, 4], 1.0).unwrap();
542        assert_eq!(hypergraph_diameter(&hypergraph), Some(2));
543    }
544
545    #[test]
546    fn testhypergraph_connected_components() {
547        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
548
549        // Create two disconnected components
550        hypergraph
551            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
552            .unwrap();
553        hypergraph
554            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
555            .unwrap();
556
557        hypergraph
558            .add_hyperedge_from_vec(vec!["D", "E"], 1.0)
559            .unwrap();
560
561        let components = hypergraph_connected_components(&hypergraph);
562        assert_eq!(components.len(), 2);
563
564        // Check component sizes
565        let sizes: Vec<usize> = components.iter().map(|c| c.len()).collect();
566        assert!(sizes.contains(&3)); // {A, B, C}
567        assert!(sizes.contains(&2)); // {D, E}
568    }
569
570    #[test]
571    fn test_ishypergraph_connected() {
572        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
573
574        // Single component
575        hypergraph
576            .add_hyperedge_from_vec(vec![1, 2, 3], 1.0)
577            .unwrap();
578        assert!(ishypergraph_connected(&hypergraph));
579
580        // Add disconnected node
581        hypergraph.add_node(4);
582        assert!(!ishypergraph_connected(&hypergraph));
583
584        // Connect the disconnected node
585        hypergraph.add_hyperedge_from_vec(vec![3, 4], 1.0).unwrap();
586        assert!(ishypergraph_connected(&hypergraph));
587    }
588
589    #[test]
590    fn test_minimum_vertex_cut() {
591        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
592
593        // Create a structure where A and D are connected through B,C
594        hypergraph
595            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
596            .unwrap();
597        hypergraph
598            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
599            .unwrap();
600        hypergraph
601            .add_hyperedge_from_vec(vec!["C", "D"], 1.0)
602            .unwrap();
603
604        let cut = minimum_vertex_cut(&hypergraph, &"A", &"D").unwrap();
605
606        assert!(!cut.partition_a.is_empty());
607        assert!(!cut.partition_b.is_empty());
608        assert!(!cut.cut_hyperedges.is_empty());
609        assert!(cut.cut_weight > 0.0);
610    }
611
612    #[test]
613    fn test_hyperedge_connectivity() {
614        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
615
616        // Direct connection
617        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
618        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &2).unwrap(), 1);
619
620        // Test direct connection in a larger hyperedge
621        hypergraph
622            .add_hyperedge_from_vec(vec![1, 3, 4], 1.2)
623            .unwrap();
624
625        // Now 1 and 4 are directly connected in the same hyperedge
626        assert!(
627            hypergraph.are_connected(&1, &4),
628            "Nodes 1 and 4 should be directly connected in the same hyperedge"
629        );
630
631        let connectivity = hyperedge_connectivity(&hypergraph, &1, &4).unwrap();
632        assert!(
633            connectivity >= 1,
634            "Expected connectivity >= 1, got {connectivity}"
635        );
636
637        // Test disconnected nodes
638        hypergraph.add_node(5); // isolated node
639        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &5).unwrap(), 0);
640
641        // Test same node
642        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &1).unwrap(), 0);
643    }
644
645    #[test]
646    fn test_generate_combinations() {
647        let items = vec![1, 2, 3, 4];
648
649        let combinations = generate_combinations(&items, 2);
650        assert_eq!(combinations.len(), 6); // C(4,2) = 6
651
652        let combinations = generate_combinations(&items, 0);
653        assert_eq!(combinations.len(), 1);
654        assert!(combinations[0].is_empty());
655
656        let combinations = generate_combinations(&items, 5);
657        assert!(combinations.is_empty());
658    }
659
660    #[test]
661    fn test_emptyhypergraph() {
662        let hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
663
664        let transversals = minimal_transversals(&hypergraph, None);
665        assert!(transversals.is_empty());
666
667        assert_eq!(hypergraph_diameter(&hypergraph), Some(0));
668
669        let components = hypergraph_connected_components(&hypergraph);
670        assert!(components.is_empty());
671
672        assert!(ishypergraph_connected(&hypergraph));
673    }
674}