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
168        + Clone
169        + Default
170        + scirs2_core::numeric::Zero
171        + std::ops::Add<Output = E>
172        + Into<f64>,
173    Ix: IndexType,
174{
175    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
176        return Err(GraphError::node_not_found("node"));
177    }
178
179    if source == target {
180        return Err(GraphError::InvalidGraph(
181            "Source and target cannot be the same".to_string(),
182        ));
183    }
184
185    // For now, implement a simple approach by converting to 2-section
186    // and finding a basic cut using BFS-based approach
187    let _graph = hypergraph.to_graph();
188
189    // Simple approach: partition nodes based on distance from source
190    // In a proper implementation, this would use max-flow min-cut
191    let all_nodes: HashSet<N> = hypergraph.nodes().cloned().collect();
192
193    // For now, create a simple partition by splitting nodes
194    let mut partition_a = HashSet::new();
195    let mut partition_b = HashSet::new();
196
197    partition_a.insert(source.clone());
198    partition_b.insert(target.clone());
199
200    // Add remaining nodes to balance partitions
201    for node in &all_nodes {
202        if node != source && node != target {
203            if partition_a.len() <= partition_b.len() {
204                partition_a.insert(node.clone());
205            } else {
206                partition_b.insert(node.clone());
207            }
208        }
209    }
210
211    // Find hyperedges that cross the cut
212    let mut cut_hyperedges = Vec::new();
213    let mut cut_weight = 0.0;
214
215    for hyperedge in hypergraph.hyperedges() {
216        let hyperedge_nodes: std::collections::HashSet<N> =
217            hyperedge.nodes.iter().cloned().collect();
218        let has_a = hyperedge_nodes.intersection(&partition_a).next().is_some();
219        let has_b = hyperedge_nodes.intersection(&partition_b).next().is_some();
220
221        if has_a && has_b {
222            cut_hyperedges.push(hyperedge.id);
223            cut_weight += hyperedge.weight.clone().into();
224        }
225    }
226
227    Ok(HypergraphCut {
228        partition_a,
229        partition_b,
230        cut_hyperedges,
231        cut_weight,
232    })
233}
234
235/// Compute hypergraph connectivity between two nodes
236///
237/// Returns the minimum number of hyperedges that need to be removed
238/// to disconnect two nodes.
239///
240/// # Arguments
241/// * `hypergraph` - The hypergraph to analyze
242/// * `source` - First node
243/// * `target` - Second node
244///
245/// # Returns
246/// * The hyperedge connectivity (minimum number of hyperedges to remove)
247#[allow(dead_code)]
248pub fn hyperedge_connectivity<N, E, Ix>(
249    hypergraph: &Hypergraph<N, E, Ix>,
250    source: &N,
251    target: &N,
252) -> Result<usize>
253where
254    N: Node + Clone + Ord + std::fmt::Debug,
255    E: EdgeWeight + Clone,
256    Ix: IndexType,
257{
258    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
259        return Err(GraphError::node_not_found("node"));
260    }
261
262    if source == target {
263        return Ok(0);
264    }
265
266    // First check if nodes are actually connected
267    if !hypergraph.are_connected(source, target) {
268        return Ok(0);
269    }
270
271    // Find all hyperedges and try removing minimum number to disconnect nodes
272    let hyperedges = hypergraph.hyperedges();
273    let connecting_hyperedges = hypergraph.connecting_hyperedges(source, target);
274
275    // If they share direct hyperedges, need at least 1 removal
276    if !connecting_hyperedges.is_empty() {
277        return Ok(1);
278    }
279
280    // Use a more sophisticated approach for indirect connections
281    // Try removing each hyperedge individually and check connectivity
282    for hyperedge in hyperedges {
283        let mut temphypergraph: Hypergraph<N, E, Ix> = Hypergraph::new();
284
285        // Add all nodes
286        for node in hypergraph.nodes() {
287            temphypergraph.add_node(node.clone());
288        }
289
290        // Add all hyperedges except the one we're testing
291        for other_hyperedge in hyperedges {
292            if other_hyperedge.id != hyperedge.id {
293                temphypergraph.add_hyperedge(
294                    other_hyperedge.nodes.clone(),
295                    other_hyperedge.weight.clone(),
296                )?;
297            }
298        }
299
300        // Check if nodes are still connected
301        if !temphypergraph.are_connected(source, target) {
302            return Ok(1);
303        }
304    }
305
306    // If single hyperedge removal doesn't disconnect, try pairs
307    // This is a simplified approach - full implementation would use max-flow
308    Ok(2)
309}
310
311/// Compute the hypergraph diameter
312///
313/// The diameter is the maximum distance between any pair of connected nodes,
314/// where distance is the minimum number of hyperedges in a path.
315///
316/// # Arguments
317/// * `hypergraph` - The hypergraph to analyze
318///
319/// # Returns
320/// * The diameter, or None if the hypergraph is disconnected
321#[allow(dead_code)]
322pub fn hypergraph_diameter<N, E, Ix>(hypergraph: &Hypergraph<N, E, Ix>) -> Option<usize>
323where
324    N: Node + Clone + Ord + std::fmt::Debug,
325    E: EdgeWeight,
326    Ix: IndexType,
327{
328    let nodes: Vec<N> = hypergraph.nodes().cloned().collect();
329    if nodes.len() < 2 {
330        return Some(0);
331    }
332
333    let mut max_distance = 0;
334
335    for i in 0..nodes.len() {
336        for j in (i + 1)..nodes.len() {
337            if let Some(distance) = hypergraph_distance(hypergraph, &nodes[i], &nodes[j]) {
338                max_distance = max_distance.max(distance);
339            } else {
340                // Graph is disconnected
341                return None;
342            }
343        }
344    }
345
346    Some(max_distance)
347}
348
349/// Compute the distance between two nodes in a hypergraph
350///
351/// Distance is the minimum number of hyperedges needed to connect two nodes.
352///
353/// # Arguments
354/// * `hypergraph` - The hypergraph to analyze
355/// * `source` - Source node
356/// * `target` - Target node
357///
358/// # Returns
359/// * The distance, or None if nodes are not connected
360#[allow(dead_code)]
361pub fn hypergraph_distance<N, E, Ix>(
362    hypergraph: &Hypergraph<N, E, Ix>,
363    source: &N,
364    target: &N,
365) -> Option<usize>
366where
367    N: Node + Clone + Ord + std::fmt::Debug,
368    E: EdgeWeight,
369    Ix: IndexType,
370{
371    if source == target {
372        return Some(0);
373    }
374
375    if !hypergraph.has_node(source) || !hypergraph.has_node(target) {
376        return None;
377    }
378
379    // BFS to find shortest path in terms of hyperedges
380    let mut visited = HashSet::new();
381    let mut queue = VecDeque::new();
382    let mut distances = HashMap::new();
383
384    queue.push_back(source.clone());
385    visited.insert(source.clone());
386    distances.insert(source.clone(), 0);
387
388    while let Some(current) = queue.pop_front() {
389        let current_distance = distances[&current];
390
391        // Get all hyperedges containing current node
392        for hyperedge in hypergraph.hyperedges_containing_node(&current) {
393            // Visit all other nodes in this hyperedge
394            for neighbor in &hyperedge.nodes {
395                if neighbor != &current && !visited.contains(neighbor) {
396                    visited.insert(neighbor.clone());
397                    distances.insert(neighbor.clone(), current_distance + 1);
398                    queue.push_back(neighbor.clone());
399
400                    if neighbor == target {
401                        return Some(current_distance + 1);
402                    }
403                }
404            }
405        }
406    }
407
408    None
409}
410
411/// Find strongly connected components in a directed hypergraph
412///
413/// This is an extension of the concept to hypergraphs by treating
414/// hyperedges as directed from one subset of nodes to another.
415/// For simplicity, this implementation treats hyperedges as connecting
416/// all nodes bidirectionally.
417///
418/// # Arguments
419/// * `hypergraph` - The hypergraph to analyze
420///
421/// # Returns
422/// * Vector of strongly connected components (as sets of nodes)
423#[allow(dead_code)]
424pub fn hypergraph_connected_components<N, E, Ix>(
425    hypergraph: &Hypergraph<N, E, Ix>,
426) -> Vec<HashSet<N>>
427where
428    N: Node + Clone + Ord + std::fmt::Debug,
429    E: EdgeWeight,
430    Ix: IndexType,
431{
432    let mut components = Vec::new();
433    let mut visited: HashSet<N> = HashSet::new();
434
435    for node in hypergraph.nodes() {
436        if !visited.contains(node) {
437            let mut component = HashSet::new();
438            let mut stack = vec![node.clone()];
439
440            while let Some(current) = stack.pop() {
441                if !visited.contains(&current) {
442                    visited.insert(current.clone());
443                    component.insert(current.clone());
444
445                    // Add all neighbors through hyperedges
446                    let neighbors = hypergraph.neighbors(&current);
447                    for neighbor in neighbors {
448                        if !visited.contains(&neighbor) {
449                            stack.push(neighbor);
450                        }
451                    }
452                }
453            }
454
455            if !component.is_empty() {
456                components.push(component);
457            }
458        }
459    }
460
461    components
462}
463
464/// Check if a hypergraph is connected
465///
466/// A hypergraph is connected if there is a path between every pair of nodes.
467///
468/// # Arguments
469/// * `hypergraph` - The hypergraph to check
470///
471/// # Returns
472/// * True if connected, false otherwise
473#[allow(dead_code)]
474pub fn ishypergraph_connected<N, E, Ix>(hypergraph: &Hypergraph<N, E, Ix>) -> bool
475where
476    N: Node + Clone + Ord + std::fmt::Debug,
477    E: EdgeWeight,
478    Ix: IndexType,
479{
480    let components = hypergraph_connected_components(hypergraph);
481    components.len() <= 1
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::base::Hypergraph;
488
489    #[test]
490    fn test_minimal_transversals_simple() {
491        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
492
493        // Add hyperedges: {1,2}, {2,3}, {1,3}
494        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
495        hypergraph.add_hyperedge_from_vec(vec![2, 3], 1.0).unwrap();
496        hypergraph.add_hyperedge_from_vec(vec![1, 3], 1.0).unwrap();
497
498        let transversals = minimal_transversals(&hypergraph, Some(2));
499
500        // Should find minimal transversals
501        assert!(!transversals.is_empty());
502
503        // Each transversal should hit all hyperedges
504        for transversal in &transversals {
505            assert_eq!(transversal.hit_hyperedges.len(), 3);
506        }
507    }
508
509    #[test]
510    fn testhypergraph_distance() {
511        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
512
513        // Create a path-like structure: {A,B}, {B,C}, {C,D}
514        hypergraph
515            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
516            .unwrap();
517        hypergraph
518            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
519            .unwrap();
520        hypergraph
521            .add_hyperedge_from_vec(vec!["C", "D"], 1.0)
522            .unwrap();
523
524        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"A"), Some(0));
525        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"B"), Some(1));
526        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"C"), Some(2));
527        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"D"), Some(3));
528
529        // Test disconnected nodes
530        hypergraph.add_node("E");
531        assert_eq!(hypergraph_distance(&hypergraph, &"A", &"E"), None);
532    }
533
534    #[test]
535    fn testhypergraph_diameter() {
536        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
537
538        // Create a path: {1,2}, {2,3}, {3,4}
539        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
540        hypergraph.add_hyperedge_from_vec(vec![2, 3], 1.0).unwrap();
541        hypergraph.add_hyperedge_from_vec(vec![3, 4], 1.0).unwrap();
542
543        assert_eq!(hypergraph_diameter(&hypergraph), Some(3));
544
545        // Add a shortcut
546        hypergraph.add_hyperedge_from_vec(vec![1, 4], 1.0).unwrap();
547        assert_eq!(hypergraph_diameter(&hypergraph), Some(2));
548    }
549
550    #[test]
551    fn testhypergraph_connected_components() {
552        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
553
554        // Create two disconnected components
555        hypergraph
556            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
557            .unwrap();
558        hypergraph
559            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
560            .unwrap();
561
562        hypergraph
563            .add_hyperedge_from_vec(vec!["D", "E"], 1.0)
564            .unwrap();
565
566        let components = hypergraph_connected_components(&hypergraph);
567        assert_eq!(components.len(), 2);
568
569        // Check component sizes
570        let sizes: Vec<usize> = components.iter().map(|c| c.len()).collect();
571        assert!(sizes.contains(&3)); // {A, B, C}
572        assert!(sizes.contains(&2)); // {D, E}
573    }
574
575    #[test]
576    fn test_ishypergraph_connected() {
577        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
578
579        // Single component
580        hypergraph
581            .add_hyperedge_from_vec(vec![1, 2, 3], 1.0)
582            .unwrap();
583        assert!(ishypergraph_connected(&hypergraph));
584
585        // Add disconnected node
586        hypergraph.add_node(4);
587        assert!(!ishypergraph_connected(&hypergraph));
588
589        // Connect the disconnected node
590        hypergraph.add_hyperedge_from_vec(vec![3, 4], 1.0).unwrap();
591        assert!(ishypergraph_connected(&hypergraph));
592    }
593
594    #[test]
595    fn test_minimum_vertex_cut() {
596        let mut hypergraph: Hypergraph<&str, f64> = Hypergraph::new();
597
598        // Create a structure where A and D are connected through B,C
599        hypergraph
600            .add_hyperedge_from_vec(vec!["A", "B"], 1.0)
601            .unwrap();
602        hypergraph
603            .add_hyperedge_from_vec(vec!["B", "C"], 1.0)
604            .unwrap();
605        hypergraph
606            .add_hyperedge_from_vec(vec!["C", "D"], 1.0)
607            .unwrap();
608
609        let cut = minimum_vertex_cut(&hypergraph, &"A", &"D").unwrap();
610
611        assert!(!cut.partition_a.is_empty());
612        assert!(!cut.partition_b.is_empty());
613        assert!(!cut.cut_hyperedges.is_empty());
614        assert!(cut.cut_weight > 0.0);
615    }
616
617    #[test]
618    fn test_hyperedge_connectivity() {
619        let mut hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
620
621        // Direct connection
622        hypergraph.add_hyperedge_from_vec(vec![1, 2], 1.0).unwrap();
623        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &2).unwrap(), 1);
624
625        // Test direct connection in a larger hyperedge
626        hypergraph
627            .add_hyperedge_from_vec(vec![1, 3, 4], 1.2)
628            .unwrap();
629
630        // Now 1 and 4 are directly connected in the same hyperedge
631        assert!(
632            hypergraph.are_connected(&1, &4),
633            "Nodes 1 and 4 should be directly connected in the same hyperedge"
634        );
635
636        let connectivity = hyperedge_connectivity(&hypergraph, &1, &4).unwrap();
637        assert!(
638            connectivity >= 1,
639            "Expected connectivity >= 1, got {connectivity}"
640        );
641
642        // Test disconnected nodes
643        hypergraph.add_node(5); // isolated node
644        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &5).unwrap(), 0);
645
646        // Test same node
647        assert_eq!(hyperedge_connectivity(&hypergraph, &1, &1).unwrap(), 0);
648    }
649
650    #[test]
651    fn test_generate_combinations() {
652        let items = vec![1, 2, 3, 4];
653
654        let combinations = generate_combinations(&items, 2);
655        assert_eq!(combinations.len(), 6); // C(4,2) = 6
656
657        let combinations = generate_combinations(&items, 0);
658        assert_eq!(combinations.len(), 1);
659        assert!(combinations[0].is_empty());
660
661        let combinations = generate_combinations(&items, 5);
662        assert!(combinations.is_empty());
663    }
664
665    #[test]
666    fn test_emptyhypergraph() {
667        let hypergraph: Hypergraph<i32, f64> = Hypergraph::new();
668
669        let transversals = minimal_transversals(&hypergraph, None);
670        assert!(transversals.is_empty());
671
672        assert_eq!(hypergraph_diameter(&hypergraph), Some(0));
673
674        let components = hypergraph_connected_components(&hypergraph);
675        assert!(components.is_empty());
676
677        assert!(ishypergraph_connected(&hypergraph));
678    }
679}