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, ProblemSizeFieldEntry, VariantDimension};
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            VariantDimension::new("graph", "DirectedGraph", &["DirectedGraph"]),
30        ],
31        module_path: module_path!(),
32        description: "Does the directed multigraph admit a directed trail using every arc exactly once?",
33        fields: &[
34            FieldInfo {
35                name: "graph",
36                type_name: "DirectedGraph",
37                description: "The directed multigraph D=(V,A); parallel arcs and loops allowed",
38            },
39        ],
40    }
41}
42
43inventory::submit! {
44    ProblemSizeFieldEntry {
45        name: "EulerianPath",
46        fields: &["num_vertices", "num_arcs"],
47    }
48}
49
50/// The Eulerian Path problem on directed multigraphs.
51///
52/// A configuration is an arc-ordering `pi`: position `t` carries the index of
53/// the arc occurrence used as the `t`-th arc of the trail.
54///
55/// `dims() = vec![m; m]` where `m = num_arcs()`. A configuration is feasible
56/// when:
57/// 1. it is a permutation of `0..m` (all values distinct, each in range), and
58/// 2. for every consecutive pair `(pi[t], pi[t+1])`, the target vertex of arc
59///    `pi[t]` equals the source vertex of arc `pi[t+1]`.
60///
61/// When `m = 0`, `dims = vec![]` and the empty configuration is the unique
62/// (trivially satisfying) witness.
63///
64/// # Example
65///
66/// ```
67/// use problemreductions::models::graph::EulerianPath;
68/// use problemreductions::topology::DirectedGraph;
69/// use problemreductions::{BruteForce, Problem, Solver};
70///
71/// // V = {0,1,2}; A = [(0,1), (0,1), (1,2), (2,0)] (parallel arc (0,1)).
72/// let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]);
73/// let problem = EulerianPath::new(graph);
74///
75/// // Witness: ordering [a_0, a_2, a_3, a_1] = (0->1)->(1->2)->(2->0)->(0->1)
76/// // traces trail 0->1->2->0->1.
77/// let witness = BruteForce::new().find_witness(&problem);
78/// assert!(witness.is_some());
79/// ```
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct EulerianPath {
82    graph: DirectedGraph,
83}
84
85impl EulerianPath {
86    /// Create a new Eulerian Path instance from a directed multigraph.
87    pub fn new(graph: DirectedGraph) -> Self {
88        Self { graph }
89    }
90
91    /// Borrow the underlying directed multigraph.
92    pub fn graph(&self) -> &DirectedGraph {
93        &self.graph
94    }
95
96    /// Number of vertices in the underlying graph.
97    pub fn num_vertices(&self) -> usize {
98        self.graph.num_vertices()
99    }
100
101    /// Number of arc occurrences in the underlying multigraph (`m = |A|`).
102    pub fn num_arcs(&self) -> usize {
103        self.graph.num_arcs()
104    }
105
106    /// Check whether an arc ordering forms a valid directed Eulerian trail.
107    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
108        is_valid_eulerian_trail(&self.graph, config)
109    }
110}
111
112impl Problem for EulerianPath {
113    const NAME: &'static str = "EulerianPath";
114    type Value = crate::types::Or;
115
116    fn variant() -> Vec<(&'static str, &'static str)> {
117        crate::variant_params![]
118    }
119
120    fn dims(&self) -> Vec<usize> {
121        let m = self.graph.num_arcs();
122        vec![m; m]
123    }
124
125    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
126        crate::types::Or(is_valid_eulerian_trail(&self.graph, config))
127    }
128}
129
130/// Decide whether `config` represents a valid directed Eulerian trail on
131/// `graph`.
132///
133/// A configuration is valid when it is a permutation of `0..m` and each
134/// consecutive pair of chosen arcs shares an endpoint (head of the previous
135/// arc equals tail of the next arc). The empty configuration on the empty
136/// multigraph (`m == 0`) is accepted.
137fn is_valid_eulerian_trail(graph: &DirectedGraph, config: &[usize]) -> bool {
138    let m = graph.num_arcs();
139    if config.len() != m {
140        return false;
141    }
142    if m == 0 {
143        return true;
144    }
145
146    // Permutation check: all values in 0..m and distinct.
147    let mut seen = vec![false; m];
148    for &idx in config {
149        if idx >= m || seen[idx] {
150            return false;
151        }
152        seen[idx] = true;
153    }
154
155    // Consecutive-arc connectivity: head(arcs[pi[t]]) == tail(arcs[pi[t+1]]).
156    let arcs = graph.arcs();
157    for window in config.windows(2) {
158        let (_prev_src, prev_tgt) = arcs[window[0]];
159        let (next_src, _next_tgt) = arcs[window[1]];
160        if prev_tgt != next_src {
161            return false;
162        }
163    }
164    true
165}
166
167#[cfg(feature = "example-db")]
168pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
169    // Canonical YES instance from the issue: V = {0,1,2},
170    // A = [(0,1), (0,1), (1,2), (2,0)] (parallel arcs a_0, a_1 between 0 and 1).
171    // Witness ordering (a_0, a_2, a_3, a_1) traces 0->1->2->0->1.
172    let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]);
173    let optimal_config = vec![0usize, 2, 3, 1];
174    vec![crate::example_db::specs::ModelExampleSpec {
175        id: "eulerian_path",
176        instance: Box::new(EulerianPath::new(graph)),
177        optimal_config,
178        optimal_value: serde_json::json!(true),
179    }]
180}
181
182crate::declare_variants! {
183    default EulerianPath => "num_vertices + num_arcs",
184}
185
186#[cfg(test)]
187#[path = "../../unit_tests/models/graph/eulerian_path.rs"]
188mod tests;