use crate::core::{Graph, IgraphResult, VertexId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EulerianClassification {
pub has_path: bool,
pub has_cycle: bool,
}
pub fn is_eulerian(graph: &Graph) -> IgraphResult<EulerianClassification> {
let n = graph.vcount();
let m = graph.ecount();
if m == 0 || n <= 1 {
return Ok(EulerianClassification {
has_path: true,
has_cycle: true,
});
}
if graph.is_directed() {
is_eulerian_directed(graph)
} else {
is_eulerian_undirected(graph)
}
}
const NONE: EulerianClassification = EulerianClassification {
has_path: false,
has_cycle: false,
};
fn at_most_one_nonsingleton_component(graph: &Graph) -> IgraphResult<bool> {
let cc = crate::algorithms::connectivity::connected_components(graph)?;
let n = graph.vcount() as usize;
let mut sizes = vec![0u32; cc.count as usize];
for &m in &cc.membership {
sizes[m as usize] = sizes[m as usize].saturating_add(1);
let _ = n; }
let big = sizes.into_iter().filter(|&s| s > 1).count();
Ok(big <= 1)
}
fn self_loop_count(graph: &Graph, v: VertexId) -> IgraphResult<usize> {
let raw = graph.neighbors(v)?.iter().filter(|&&u| u == v).count();
if graph.is_directed() {
Ok(raw)
} else {
Ok(raw / 2)
}
}
fn is_eulerian_undirected(graph: &Graph) -> IgraphResult<EulerianClassification> {
if !at_most_one_nonsingleton_component(graph)? {
return Ok(NONE);
}
let n = graph.vcount();
let mut odd: u32 = 0;
let mut singleton_self_loop: u32 = 0; let mut has_nonsingleton_edge: u32 = 0;
for v in 0..n {
let total_deg = graph.degree(v)?; if total_deg == 0 {
continue;
}
let loops = self_loop_count(graph, v)?;
let nonloop_deg = total_deg - 2 * loops;
if nonloop_deg == 0 {
singleton_self_loop = singleton_self_loop.saturating_add(1);
} else {
has_nonsingleton_edge = 1;
if total_deg % 2 != 0 {
odd = odd.saturating_add(1);
}
}
if singleton_self_loop + has_nonsingleton_edge > 1 {
return Ok(NONE);
}
}
let (has_path, has_cycle) = match odd.cmp(&2) {
std::cmp::Ordering::Greater => (false, false),
std::cmp::Ordering::Equal => (true, false),
std::cmp::Ordering::Less => (true, true),
};
Ok(EulerianClassification {
has_path,
has_cycle,
})
}
fn is_eulerian_directed(graph: &Graph) -> IgraphResult<EulerianClassification> {
if !at_most_one_nonsingleton_component(graph)? {
return Ok(NONE);
}
let n = graph.vcount();
let mut incoming_excess: u32 = 0;
let mut outgoing_excess: u32 = 0;
let mut singleton_self_loop: u32 = 0;
let mut has_nonsingleton_edge: u32 = 0;
for v in 0..n {
let out_deg = graph.out_neighbors_vec(v)?.len();
let in_deg = graph.in_neighbors_vec(v)?.len();
if out_deg + in_deg == 0 {
continue;
}
let loops = self_loop_count(graph, v)?;
let nonloop_deg = out_deg + in_deg - 2 * loops;
if nonloop_deg == 0 {
singleton_self_loop = singleton_self_loop.saturating_add(1);
} else {
has_nonsingleton_edge = 1;
}
if singleton_self_loop + has_nonsingleton_edge > 1 {
return Ok(NONE);
}
if in_deg == out_deg {
continue;
}
if in_deg > out_deg {
let diff = u32::try_from(in_deg - out_deg)
.map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
incoming_excess = incoming_excess.saturating_add(diff);
} else {
let diff = u32::try_from(out_deg - in_deg)
.map_err(|_| crate::core::IgraphError::Internal("degree overflow"))?;
outgoing_excess = outgoing_excess.saturating_add(diff);
}
if incoming_excess > 1 || outgoing_excess > 1 {
return Ok(NONE);
}
}
let has_cycle = incoming_excess == 0 && outgoing_excess == 0;
Ok(EulerianClassification {
has_path: true,
has_cycle,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn classify(graph: &Graph) -> EulerianClassification {
is_eulerian(graph).unwrap()
}
#[test]
fn empty_graph_is_eulerian() {
let g = Graph::with_vertices(0);
assert_eq!(
classify(&g),
EulerianClassification {
has_path: true,
has_cycle: true
}
);
}
#[test]
fn single_isolated_vertex_is_eulerian() {
let g = Graph::with_vertices(1);
assert_eq!(
classify(&g),
EulerianClassification {
has_path: true,
has_cycle: true
}
);
}
#[test]
fn isolated_vertices_no_edges_are_eulerian() {
let g = Graph::with_vertices(5);
assert_eq!(
classify(&g),
EulerianClassification {
has_path: true,
has_cycle: true
}
);
}
#[test]
fn undirected_path_has_path_no_cycle() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let r = classify(&g);
assert!(r.has_path);
assert!(!r.has_cycle);
}
#[test]
fn undirected_triangle_has_cycle_and_path() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let r = classify(&g);
assert!(r.has_path);
assert!(r.has_cycle);
}
#[test]
fn undirected_disconnected_components_are_not_eulerian() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
let r = classify(&g);
assert!(!r.has_path);
assert!(!r.has_cycle);
}
#[test]
fn undirected_too_many_odd_vertices_is_not_eulerian() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
let r = classify(&g);
assert!(!r.has_path);
assert!(!r.has_cycle);
}
#[test]
fn undirected_self_loop_on_otherwise_eulerian_graph() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let r = classify(&g);
assert!(r.has_path);
assert!(r.has_cycle);
}
#[test]
fn directed_cycle_has_cycle() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let r = classify(&g);
assert!(r.has_path);
assert!(r.has_cycle);
}
#[test]
fn directed_path_has_path_no_cycle() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let r = classify(&g);
assert!(r.has_path);
assert!(!r.has_cycle);
}
#[test]
fn directed_too_imbalanced_is_not_eulerian() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
let r = classify(&g);
assert!(!r.has_path);
assert!(!r.has_cycle);
}
}