Skip to main content

problemreductions/models/graph/
eulerian_path.rs

1//! Eulerian Path problem implementation.
2//!
3//! Given a finite directed multigraph `D = (V, A)` with loops and parallel arcs
4//! allowed, determine whether there exists a directed trail that uses every
5//! arc in `A` exactly once.
6//!
7//! Conventions:
8//! - repeated arc occurrences are distinguished,
9//! - loops are allowed,
10//! - isolated vertices are allowed and ignored,
11//! - a closed trail is accepted,
12//! - the empty-arc instance is accepted, witnessed by the empty trail.
13//!
14//! The problem is a satisfaction (witness) problem and is solvable in linear
15//! time `O(num_vertices + num_arcs)` by the classical degree-balance plus
16//! Hierholzer construction (Bang-Jensen & Gutin 2009; Ebert 1988).
17
18use crate::registry::{FieldInfo, ProblemSchemaEntry};
19use crate::topology::DirectedGraph;
20use crate::traits::Problem;
21use serde::{Deserialize, Serialize};
22
23inventory::submit! {
24    ProblemSchemaEntry {
25        name: "EulerianPath",
26        display_name: "Eulerian Path",
27        aliases: &[],
28        dimensions: &[],
29        category: crate::registry::ProblemCategory::Graph,
30        module_path: module_path!(),
31        description: "Does the directed multigraph admit a directed trail using every arc exactly once?",
32        fields: &[
33            FieldInfo {
34                name: "graph",
35                type_name: "DirectedGraph",
36                description: "The directed multigraph D=(V,A); parallel arcs and loops allowed",
37            },
38        ],
39    }
40}
41
42/// The Eulerian Path problem on directed multigraphs.
43///
44/// A configuration is an arc-ordering `pi`: position `t` carries the index of
45/// the arc occurrence used as the `t`-th arc of the trail.
46///
47/// `dims() = vec![m; m]` where `m = num_arcs()`. A configuration is feasible
48/// when:
49/// 1. it is a permutation of `0..m` (all values distinct, each in range), and
50/// 2. for every consecutive pair `(pi[t], pi[t+1])`, the target vertex of arc
51///    `pi[t]` equals the source vertex of arc `pi[t+1]`.
52///
53/// When `m = 0`, `dims = vec![]` and the empty configuration is the unique
54/// (trivially satisfying) witness.
55///
56/// # Example
57///
58/// ```
59/// use problemreductions::models::graph::EulerianPath;
60/// use problemreductions::topology::DirectedGraph;
61/// use problemreductions::{BruteForce, Problem};
62///
63/// // V = {0,1,2}; A = [(0,1), (0,1), (1,2), (2,0)] (parallel arc (0,1)).
64/// let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]);
65/// let problem = EulerianPath::new(graph);
66///
67/// // Witness: ordering [a_0, a_2, a_3, a_1] = (0->1)->(1->2)->(2->0)->(0->1)
68/// // traces trail 0->1->2->0->1.
69/// let witness = BruteForce::new().solve(&problem).unwrap();
70/// assert!(witness.is_some());
71/// ```
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct EulerianPath {
74    graph: DirectedGraph,
75}
76
77impl EulerianPath {
78    /// Create a new Eulerian Path instance from a directed multigraph.
79    pub fn new(graph: DirectedGraph) -> Self {
80        Self { graph }
81    }
82
83    /// Borrow the underlying directed multigraph.
84    pub fn graph(&self) -> &DirectedGraph {
85        &self.graph
86    }
87
88    /// Number of vertices in the underlying graph.
89    pub fn num_vertices(&self) -> usize {
90        self.graph.num_vertices()
91    }
92
93    /// Number of arc occurrences in the underlying multigraph (`m = |A|`).
94    pub fn num_arcs(&self) -> usize {
95        self.graph.num_arcs()
96    }
97
98    /// Check whether an arc ordering forms a valid directed Eulerian trail.
99    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
100        is_valid_eulerian_trail(&self.graph, config)
101    }
102}
103
104impl Problem for EulerianPath {
105    const NAME: &'static str = "EulerianPath";
106    type Solution = Vec<usize>;
107    type Value = crate::types::Or;
108
109    crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
110
111    fn variant() -> Vec<(&'static str, &'static str)> {
112        crate::variant_params![]
113    }
114
115    fn evaluate(
116        &self,
117        config: &Self::Solution,
118    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
119        let m = self.graph.num_arcs();
120        if config.len() != m {
121            return Err(crate::traits::EvaluationError::InvalidConfiguration(
122                "arc ordering length does not match the graph".into(),
123            ));
124        }
125        if config.iter().any(|&arc| arc >= m) {
126            return Err(crate::traits::EvaluationError::InvalidConfiguration(
127                "arc ordering contains an out-of-range arc".into(),
128            ));
129        }
130        Ok(crate::types::Or(is_valid_eulerian_trail(
131            &self.graph,
132            config,
133        )))
134    }
135}
136
137impl crate::solvers::BruteForceProblem for EulerianPath {
138    fn dimensions(&self) -> Vec<usize> {
139        let m = self.graph.num_arcs();
140        vec![m; m]
141    }
142}
143
144/// Decide whether `config` represents a valid directed Eulerian trail on
145/// `graph`.
146///
147/// A configuration is valid when it is a permutation of `0..m` and each
148/// consecutive pair of chosen arcs shares an endpoint (head of the previous
149/// arc equals tail of the next arc). The empty configuration on the empty
150/// multigraph (`m == 0`) is accepted.
151fn is_valid_eulerian_trail(graph: &DirectedGraph, config: &[usize]) -> bool {
152    let m = graph.num_arcs();
153    if config.len() != m {
154        return false;
155    }
156    if m == 0 {
157        return true;
158    }
159
160    // Permutation check: all values in 0..m and distinct.
161    let mut seen = vec![false; m];
162    for &idx in config {
163        if idx >= m || seen[idx] {
164            return false;
165        }
166        seen[idx] = true;
167    }
168
169    // Consecutive-arc connectivity: head(arcs[pi[t]]) == tail(arcs[pi[t+1]]).
170    let arcs = graph.arcs();
171    for window in config.windows(2) {
172        let (_prev_src, prev_tgt) = arcs[window[0]];
173        let (next_src, _next_tgt) = arcs[window[1]];
174        if prev_tgt != next_src {
175            return false;
176        }
177    }
178    true
179}
180
181#[cfg(feature = "example-db")]
182pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
183    // Canonical YES instance from the issue: V = {0,1,2},
184    // A = [(0,1), (0,1), (1,2), (2,0)] (parallel arcs a_0, a_1 between 0 and 1).
185    // Witness ordering (a_0, a_2, a_3, a_1) traces 0->1->2->0->1.
186    let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]);
187    let optimal_config = vec![0usize, 2, 3, 1];
188    vec![crate::example_db::specs::ModelExampleSpec {
189        id: "eulerian_path",
190        instance: Box::new(EulerianPath::new(graph)),
191        optimal_config: serde_json::to_value(optimal_config)
192            .expect("solution serialization must succeed"),
193        optimal_value: serde_json::json!(true),
194    }]
195}
196
197crate::declare_variants! {
198    default EulerianPath => "num_vertices + num_arcs",
199}
200
201crate::register_brute_force! {
202    EulerianPath,
203}
204
205#[cfg(test)]
206#[path = "../../unit_tests/models/graph/eulerian_path.rs"]
207mod tests;