rust_igraph/algorithms/paths/
eulerian.rs1use crate::core::{Graph, IgraphResult, VertexId};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct EulerianClassification {
29 pub has_path: bool,
31 pub has_cycle: bool,
34}
35
36pub fn is_eulerian(graph: &Graph) -> IgraphResult<EulerianClassification> {
62 let n = graph.vcount();
63 let m = graph.ecount();
64 if m == 0 || n <= 1 {
65 return Ok(EulerianClassification {
66 has_path: true,
67 has_cycle: true,
68 });
69 }
70 if graph.is_directed() {
71 is_eulerian_directed(graph)
72 } else {
73 is_eulerian_undirected(graph)
74 }
75}
76
77const NONE: EulerianClassification = EulerianClassification {
78 has_path: false,
79 has_cycle: false,
80};
81
82fn at_most_one_nonsingleton_component(graph: &Graph) -> IgraphResult<bool> {
87 let cc = crate::algorithms::connectivity::connected_components(graph)?;
88 let n = graph.vcount() as usize;
89 let mut sizes = vec![0u32; cc.count as usize];
90 for &m in &cc.membership {
91 sizes[m as usize] = sizes[m as usize].saturating_add(1);
92 let _ = n; }
94 let big = sizes.into_iter().filter(|&s| s > 1).count();
95 Ok(big <= 1)
96}
97
98fn self_loop_count(graph: &Graph, v: VertexId) -> IgraphResult<usize> {
100 let raw = graph.neighbors(v)?.iter().filter(|&&u| u == v).count();
103 if graph.is_directed() {
104 Ok(raw)
105 } else {
106 Ok(raw / 2)
107 }
108}
109
110fn is_eulerian_undirected(graph: &Graph) -> IgraphResult<EulerianClassification> {
111 if !at_most_one_nonsingleton_component(graph)? {
112 return Ok(NONE);
113 }
114
115 let n = graph.vcount();
116
117 let mut odd: u32 = 0;
118 let mut singleton_self_loop: u32 = 0; let mut has_nonsingleton_edge: u32 = 0; for v in 0..n {
122 let total_deg = graph.degree(v)?; if total_deg == 0 {
124 continue;
125 }
126 let loops = self_loop_count(graph, v)?;
127 let nonloop_deg = total_deg - 2 * loops;
130 if nonloop_deg == 0 {
131 singleton_self_loop = singleton_self_loop.saturating_add(1);
133 } else {
134 has_nonsingleton_edge = 1;
135 if total_deg % 2 != 0 {
138 odd = odd.saturating_add(1);
139 }
140 }
141 if singleton_self_loop + has_nonsingleton_edge > 1 {
142 return Ok(NONE);
143 }
144 }
145
146 let (has_path, has_cycle) = match odd.cmp(&2) {
147 std::cmp::Ordering::Greater => (false, false),
148 std::cmp::Ordering::Equal => (true, false),
149 std::cmp::Ordering::Less => (true, true),
150 };
151 Ok(EulerianClassification {
152 has_path,
153 has_cycle,
154 })
155}
156
157fn is_eulerian_directed(graph: &Graph) -> IgraphResult<EulerianClassification> {
158 if !at_most_one_nonsingleton_component(graph)? {
159 return Ok(NONE);
160 }
161 let n = graph.vcount();
162
163 let mut incoming_excess: u32 = 0;
164 let mut outgoing_excess: u32 = 0;
165 let mut singleton_self_loop: u32 = 0;
166 let mut has_nonsingleton_edge: u32 = 0;
167
168 for v in 0..n {
169 let out_deg = graph.out_neighbors_vec(v)?.len();
170 let in_deg = graph.in_neighbors_vec(v)?.len();
171 if out_deg + in_deg == 0 {
172 continue;
173 }
174 let loops = self_loop_count(graph, v)?;
175 let nonloop_deg = out_deg + in_deg - 2 * loops;
178 if nonloop_deg == 0 {
179 singleton_self_loop = singleton_self_loop.saturating_add(1);
180 } else {
181 has_nonsingleton_edge = 1;
182 }
183 if singleton_self_loop + has_nonsingleton_edge > 1 {
184 return Ok(NONE);
185 }
186
187 if in_deg == out_deg {
190 continue;
191 }
192 if in_deg > out_deg {
193 let diff = u32::try_from(in_deg - out_deg)
194 .map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
195 incoming_excess = incoming_excess.saturating_add(diff);
196 } else {
197 let diff = u32::try_from(out_deg - in_deg)
198 .map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
199 outgoing_excess = outgoing_excess.saturating_add(diff);
200 }
201 if incoming_excess > 1 || outgoing_excess > 1 {
202 return Ok(NONE);
203 }
204 }
205
206 let has_cycle = incoming_excess == 0 && outgoing_excess == 0;
207 Ok(EulerianClassification {
210 has_path: true,
211 has_cycle,
212 })
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn classify(graph: &Graph) -> EulerianClassification {
220 is_eulerian(graph).unwrap()
221 }
222
223 #[test]
224 fn empty_graph_is_eulerian() {
225 let g = Graph::with_vertices(0);
226 assert_eq!(
227 classify(&g),
228 EulerianClassification {
229 has_path: true,
230 has_cycle: true
231 }
232 );
233 }
234
235 #[test]
236 fn single_isolated_vertex_is_eulerian() {
237 let g = Graph::with_vertices(1);
238 assert_eq!(
239 classify(&g),
240 EulerianClassification {
241 has_path: true,
242 has_cycle: true
243 }
244 );
245 }
246
247 #[test]
248 fn isolated_vertices_no_edges_are_eulerian() {
249 let g = Graph::with_vertices(5);
250 assert_eq!(
251 classify(&g),
252 EulerianClassification {
253 has_path: true,
254 has_cycle: true
255 }
256 );
257 }
258
259 #[test]
260 fn undirected_path_has_path_no_cycle() {
261 let mut g = Graph::with_vertices(3);
262 g.add_edge(0, 1).unwrap();
263 g.add_edge(1, 2).unwrap();
264 let r = classify(&g);
265 assert!(r.has_path);
266 assert!(!r.has_cycle);
267 }
268
269 #[test]
270 fn undirected_triangle_has_cycle_and_path() {
271 let mut g = Graph::with_vertices(3);
272 g.add_edge(0, 1).unwrap();
273 g.add_edge(1, 2).unwrap();
274 g.add_edge(2, 0).unwrap();
275 let r = classify(&g);
276 assert!(r.has_path);
277 assert!(r.has_cycle);
278 }
279
280 #[test]
281 fn undirected_disconnected_components_are_not_eulerian() {
282 let mut g = Graph::with_vertices(4);
283 g.add_edge(0, 1).unwrap();
284 g.add_edge(2, 3).unwrap();
285 let r = classify(&g);
286 assert!(!r.has_path);
287 assert!(!r.has_cycle);
288 }
289
290 #[test]
291 fn undirected_too_many_odd_vertices_is_not_eulerian() {
292 let mut g = Graph::with_vertices(4);
294 for u in 0..4u32 {
295 for v in (u + 1)..4 {
296 g.add_edge(u, v).unwrap();
297 }
298 }
299 let r = classify(&g);
300 assert!(!r.has_path);
301 assert!(!r.has_cycle);
302 }
303
304 #[test]
305 fn undirected_self_loop_on_otherwise_eulerian_graph() {
306 let mut g = Graph::with_vertices(3);
309 g.add_edge(0, 0).unwrap();
310 g.add_edge(0, 1).unwrap();
311 g.add_edge(1, 2).unwrap();
312 g.add_edge(2, 0).unwrap();
313 let r = classify(&g);
314 assert!(r.has_path);
315 assert!(r.has_cycle);
316 }
317
318 #[test]
319 fn directed_cycle_has_cycle() {
320 let mut g = Graph::new(3, true).unwrap();
322 g.add_edge(0, 1).unwrap();
323 g.add_edge(1, 2).unwrap();
324 g.add_edge(2, 0).unwrap();
325 let r = classify(&g);
326 assert!(r.has_path);
327 assert!(r.has_cycle);
328 }
329
330 #[test]
331 fn directed_path_has_path_no_cycle() {
332 let mut g = Graph::new(3, true).unwrap();
334 g.add_edge(0, 1).unwrap();
335 g.add_edge(1, 2).unwrap();
336 let r = classify(&g);
337 assert!(r.has_path);
338 assert!(!r.has_cycle);
339 }
340
341 #[test]
342 fn directed_too_imbalanced_is_not_eulerian() {
343 let mut g = Graph::new(3, true).unwrap();
345 g.add_edge(0, 1).unwrap();
346 g.add_edge(0, 2).unwrap();
347 let r = classify(&g);
348 assert!(!r.has_path);
349 assert!(!r.has_cycle);
350 }
351}