Skip to main content

uni_algo/algo/algorithms/
bipartite_check.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Bipartite Check Algorithm.
5//!
6//! Checks if a graph is bipartite (2-colorable) using BFS.
7//! Returns `true` if bipartite, and the partition (0 or 1) for each node.
8
9use crate::algo::GraphProjection;
10use crate::algo::algorithms::Algorithm;
11use uni_common::core::id::Vid;
12
13pub struct BipartiteCheck;
14
15#[derive(Debug, Clone, Default)]
16pub struct BipartiteCheckConfig {}
17
18pub struct BipartiteCheckResult {
19    pub is_bipartite: bool,
20    pub partition: Vec<(Vid, u8)>, // (node, color: 0 or 1)
21}
22
23impl Algorithm for BipartiteCheck {
24    type Config = BipartiteCheckConfig;
25    type Result = BipartiteCheckResult;
26
27    fn name() -> &'static str {
28        "bipartite_check"
29    }
30
31    fn run(graph: &GraphProjection, _config: Self::Config) -> Self::Result {
32        let n = graph.vertex_count();
33        if n == 0 {
34            return BipartiteCheckResult {
35                is_bipartite: true,
36                partition: Vec::new(),
37            };
38        }
39
40        // 0: uncolored, 1: color A, 2: color B
41        let mut colors = vec![0u8; n];
42        let mut is_bipartite = true;
43
44        for start_node in 0..n {
45            if colors[start_node] != 0 {
46                continue;
47            }
48
49            let mut queue = std::collections::VecDeque::new();
50            queue.push_back(start_node as u32);
51            colors[start_node] = 1;
52
53            while let Some(u) = queue.pop_front() {
54                let current_color = colors[u as usize];
55                let next_color = if current_color == 1 { 2 } else { 1 };
56
57                // Check out-neighbors (treat as undirected for bipartite check typically,
58                // but if strictly directed, we only check forward edges?
59                // Bipartite usually implies underlying undirected graph structure.
60                // If strictly directed, a cycle might not matter if directions align.
61                // Standard definition: "vertices can be divided into two disjoint and independent sets"
62                // This usually applies to undirected edges.
63                // Let's assume undirected semantics by checking both directions if possible,
64                // or just outgoing if directed graph semantics.
65                // Uni graphs are directed. But bipartite check usually implies structure.
66                // Let's check outgoing edges. If graph represents undirected, user should have added reverse edges.
67                // Actually, `GraphProjection` can have reverse edges if requested.
68                // If `include_reverse` is false, we only check outgoing.
69
70                for &v in graph.out_neighbors(u) {
71                    if colors[v as usize] == 0 {
72                        colors[v as usize] = next_color;
73                        queue.push_back(v);
74                    } else if colors[v as usize] == current_color {
75                        is_bipartite = false;
76                        // Don't break immediately if we want to color the rest of the component?
77                        // But result is invalid.
78                        // We can stop or continue. Let's continue to fill partition map best-effort or stop?
79                        // The result says "is_bipartite: false". Partition might be partial/invalid.
80                        // Let's continue to process this component to finish coloring for debug, but mark false.
81                    }
82                }
83
84                // If we had reverse edges (undirected check), we would check in_neighbors too.
85                if graph.has_reverse() {
86                    for &v in graph.in_neighbors(u) {
87                        if colors[v as usize] == 0 {
88                            colors[v as usize] = next_color;
89                            queue.push_back(v);
90                        } else if colors[v as usize] == current_color {
91                            is_bipartite = false;
92                        }
93                    }
94                }
95            }
96        }
97
98        let partition = if is_bipartite {
99            colors
100                .into_iter()
101                .enumerate()
102                .filter(|(_, c)| *c != 0)
103                .map(|(i, c)| (graph.to_vid(i as u32), c - 1))
104                .collect()
105        } else {
106            Vec::new() // Or return partial partition?
107        };
108
109        BipartiteCheckResult {
110            is_bipartite,
111            partition,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::algo::test_utils::build_test_graph;
120
121    #[test]
122    fn test_bipartite_check_true() {
123        // 0 -> 1, 1 -> 2 (0 and 2 are color A, 1 is color B)
124        let vids = vec![Vid::from(0), Vid::from(1), Vid::from(2)];
125        let edges = vec![(Vid::from(0), Vid::from(1)), (Vid::from(1), Vid::from(2))];
126        let graph = build_test_graph(vids, edges);
127
128        let result = BipartiteCheck::run(&graph, BipartiteCheckConfig::default());
129        assert!(result.is_bipartite);
130        assert_eq!(result.partition.len(), 3);
131    }
132
133    #[test]
134    fn test_bipartite_check_false_triangle() {
135        // 0 -> 1 -> 2 -> 0
136        let vids = vec![Vid::from(0), Vid::from(1), Vid::from(2)];
137        let edges = vec![
138            (Vid::from(0), Vid::from(1)),
139            (Vid::from(1), Vid::from(2)),
140            (Vid::from(2), Vid::from(0)),
141        ];
142        let graph = build_test_graph(vids, edges);
143
144        let result = BipartiteCheck::run(&graph, BipartiteCheckConfig::default());
145        assert!(!result.is_bipartite);
146    }
147}