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;