Skip to main content

problemreductions/models/graph/
rural_postman.rs

1//! Rural Postman problem implementation.
2//!
3//! The Rural Postman problem asks for a minimum-cost circuit in a graph
4//! that includes each edge in a required subset E'.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Min, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "RuralPostman",
17        display_name: "Rural Postman",
18        aliases: &["RPP"],
19        dimensions: &[
20            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
21            VariantDimension::new("weight", "i64", &["i64"]),
22        ],
23        category: crate::registry::ProblemCategory::Graph,
24        module_path: module_path!(),
25        description: "Find a minimum-cost circuit covering all required edges (Rural Postman Problem)",
26        fields: RuralPostmanCreateSpec::FIELDS,
27    }
28}
29
30/// The Rural Postman problem.
31///
32/// Given a weighted graph G = (V, E) with edge lengths l(e) and
33/// a subset E' ⊆ E of required edges, find a minimum-cost circuit
34/// (closed walk) in G that includes each edge in E'.
35///
36/// # Representation
37///
38/// Each edge is assigned a multiplicity variable:
39/// - 0: edge is not traversed
40/// - 1: edge is traversed once
41/// - 2: edge is traversed twice
42///
43/// A valid circuit requires:
44/// - All required edges have multiplicity ≥ 1
45/// - All vertices have even degree (sum of multiplicities of incident edges)
46/// - Edges with multiplicity > 0 form a connected subgraph
47///
48/// Note: In an optimal RPP solution on undirected graphs, each edge is
49/// traversed at most twice, so multiplicity ∈ {0, 1, 2} is sufficient.
50///
51/// # Type Parameters
52///
53/// * `G` - The graph type (e.g., `SimpleGraph`)
54/// * `W` - The weight type for edge lengths (e.g., `i64`, `f64`)
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(try_from = "RuralPostmanData<G, W>")]
57#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))]
58pub struct RuralPostman<G, W: WeightElement> {
59    /// The underlying graph.
60    graph: G,
61    /// Lengths for each edge (in edge index order).
62    edge_lengths: Vec<W>,
63    /// Indices of required edges (subset E' ⊆ E).
64    required_edges: Vec<usize>,
65}
66
67#[derive(Deserialize)]
68#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))]
69struct RuralPostmanData<G, W: WeightElement> {
70    graph: G,
71    edge_lengths: Vec<W>,
72    required_edges: Vec<usize>,
73}
74
75impl<G, W> TryFrom<RuralPostmanData<G, W>> for RuralPostman<G, W>
76where
77    G: Graph,
78    W: WeightElement,
79{
80    type Error = crate::registry::ConstructionError;
81
82    fn try_from(data: RuralPostmanData<G, W>) -> Result<Self, Self::Error> {
83        Self::try_new(data.graph, data.edge_lengths, data.required_edges)
84    }
85}
86
87#[derive(Debug, Deserialize, crate::CreateSpec)]
88struct RuralPostmanCreateSpec {
89    #[create(codec = "edge-list")]
90    graph: Vec<(usize, usize)>,
91    num_vertices: Option<usize>,
92    #[create(codec = "comma-separated")]
93    edge_weights: Option<Vec<i64>>,
94    #[create(codec = "comma-separated")]
95    required_edges: Vec<usize>,
96}
97
98impl TryFrom<RuralPostmanCreateSpec> for RuralPostman<SimpleGraph, i64> {
99    type Error = crate::registry::ConstructionError;
100
101    fn try_from(spec: RuralPostmanCreateSpec) -> Result<Self, Self::Error> {
102        let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
103        let edge_lengths = spec
104            .edge_weights
105            .unwrap_or_else(|| vec![1; graph.num_edges()]);
106        Self::try_new(graph, edge_lengths, spec.required_edges)
107    }
108}
109
110fn simple_graph_from_create(
111    edges: Vec<(usize, usize)>,
112    num_vertices: Option<usize>,
113) -> Result<SimpleGraph, crate::registry::ConstructionError> {
114    if edges.is_empty() && num_vertices.is_none() {
115        return Err("num_vertices is required for an empty graph"
116            .to_string()
117            .into());
118    }
119    for (index, &(u, v)) in edges.iter().enumerate() {
120        if u == v {
121            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
122        }
123    }
124    let inferred = edges
125        .iter()
126        .flat_map(|&(u, v)| [u, v])
127        .max()
128        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
129        .transpose()?
130        .unwrap_or(0);
131    let num_vertices = num_vertices.unwrap_or(inferred);
132    if num_vertices < inferred {
133        return Err(format!(
134            "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
135        )
136        .into());
137    }
138    Ok(SimpleGraph::new(num_vertices, edges))
139}
140
141impl<G: Graph, W: WeightElement> RuralPostman<G, W> {
142    /// Create a new RuralPostman problem.
143    ///
144    /// # Panics
145    /// Panics if edge_lengths length does not match graph edges,
146    /// or if any required edge index is out of bounds.
147    pub fn new(graph: G, edge_lengths: Vec<W>, required_edges: Vec<usize>) -> Self {
148        Self::try_new(graph, edge_lengths, required_edges).unwrap_or_else(|error| panic!("{error}"))
149    }
150
151    fn try_new(
152        graph: G,
153        edge_lengths: Vec<W>,
154        required_edges: Vec<usize>,
155    ) -> Result<Self, crate::registry::ConstructionError> {
156        Self::check_weights(&graph, &edge_lengths)?;
157        for &idx in &required_edges {
158            if idx >= graph.num_edges() {
159                return Err(format!(
160                    "required edge index {} out of bounds (graph has {} edges)",
161                    idx,
162                    graph.num_edges()
163                )
164                .into());
165            }
166        }
167        Ok(Self {
168            graph,
169            edge_lengths,
170            required_edges,
171        })
172    }
173
174    /// Get a reference to the underlying graph.
175    pub fn graph(&self) -> &G {
176        &self.graph
177    }
178
179    /// Get the edge lengths.
180    pub fn edge_lengths(&self) -> &[W] {
181        &self.edge_lengths
182    }
183
184    /// Get the required edge indices.
185    pub fn required_edges(&self) -> &[usize] {
186        &self.required_edges
187    }
188
189    /// Get the number of vertices in the underlying graph.
190    pub fn num_vertices(&self) -> usize {
191        self.graph.num_vertices()
192    }
193
194    /// Get the number of edges in the underlying graph.
195    pub fn num_edges(&self) -> usize {
196        self.graph.num_edges()
197    }
198
199    /// Get the number of required edges.
200    pub fn num_required_edges(&self) -> usize {
201        self.required_edges.len()
202    }
203
204    /// Set new edge lengths.
205    pub fn set_weights(&mut self, weights: Vec<W>) {
206        Self::check_weights(&self.graph, &weights).unwrap_or_else(|error| panic!("{error}"));
207        self.edge_lengths = weights;
208    }
209
210    fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> {
211        if weights.len() != graph.num_edges() {
212            return Err(crate::registry::ConstructionError::length_mismatch(
213                "weights",
214                weights.len(),
215                graph.num_edges(),
216            ));
217        }
218        Ok(())
219    }
220
221    /// Get the edge lengths as a Vec.
222    pub fn weights(&self) -> Vec<W> {
223        self.edge_lengths.clone()
224    }
225
226    /// Check if the problem uses a non-unit weight type.
227    pub fn is_weighted(&self) -> bool {
228        !W::IS_UNIT
229    }
230
231    /// Check if a configuration represents a valid circuit covering all required edges.
232    /// Returns `Some(cost)` if valid, `None` otherwise.
233    ///
234    /// Each `config[i]` is the multiplicity (number of traversals) of edge `i`.
235    pub fn is_valid_solution(
236        &self,
237        config: &[usize],
238    ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
239        if config.len() != self.graph.num_edges() {
240            return Ok(None);
241        }
242
243        let edges = self.graph.edges();
244        let n = self.graph.num_vertices();
245
246        // Check all required edges are traversed at least once
247        for &req_idx in &self.required_edges {
248            if config[req_idx] == 0 {
249                return Ok(None);
250            }
251        }
252
253        // Compute degree of each vertex (sum of multiplicities of incident edges)
254        let mut degree = vec![0usize; n];
255        let mut has_edges = false;
256        for (idx, &mult) in config.iter().enumerate() {
257            if mult > 0 {
258                let (u, v) = edges[idx];
259                degree[u] += mult;
260                degree[v] += mult;
261                has_edges = true;
262            }
263        }
264
265        // No edges used: only valid if no required edges
266        if !has_edges {
267            if self.required_edges.is_empty() {
268                return Ok(Some(W::Sum::zero()));
269            } else {
270                return Ok(None);
271            }
272        }
273
274        // All vertices must have even degree (Eulerian condition)
275        for &d in &degree {
276            if d % 2 != 0 {
277                return Ok(None);
278            }
279        }
280
281        // Edges with multiplicity > 0 must form a connected subgraph
282        // (considering only vertices with degree > 0)
283        let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
284        let mut first_vertex = None;
285        for (idx, &mult) in config.iter().enumerate() {
286            if mult > 0 {
287                let (u, v) = edges[idx];
288                adj[u].push(v);
289                adj[v].push(u);
290                if first_vertex.is_none() {
291                    first_vertex = Some(u);
292                }
293            }
294        }
295
296        let first = match first_vertex {
297            Some(v) => v,
298            None => {
299                if self.required_edges.is_empty() {
300                    return Ok(Some(W::Sum::zero()));
301                } else {
302                    return Ok(None);
303                }
304            }
305        };
306
307        let mut visited = vec![false; n];
308        let mut queue = VecDeque::new();
309        visited[first] = true;
310        queue.push_back(first);
311
312        while let Some(node) = queue.pop_front() {
313            for &neighbor in &adj[node] {
314                if !visited[neighbor] {
315                    visited[neighbor] = true;
316                    queue.push_back(neighbor);
317                }
318            }
319        }
320
321        // All vertices with degree > 0 must be visited
322        for v in 0..n {
323            if degree[v] > 0 && !visited[v] {
324                return Ok(None);
325            }
326        }
327
328        // Compute total cost (sum of multiplicity × edge length)
329        let mut total = W::Sum::zero();
330        for (idx, &mult) in config.iter().enumerate() {
331            for _ in 0..mult {
332                total = W::checked_add_to_sum(
333                    total,
334                    self.edge_lengths[idx].to_sum(),
335                    "summing rural postman edge lengths",
336                )?;
337            }
338        }
339
340        Ok(Some(total))
341    }
342}
343
344impl<G, W> Problem for RuralPostman<G, W>
345where
346    G: Graph + crate::variant::VariantParam,
347    W: WeightElement + crate::variant::VariantParam,
348{
349    const NAME: &'static str = "RuralPostman";
350    type Solution = Vec<usize>;
351    type Value = Min<W::Sum>;
352
353    crate::problem_parameters![
354        ("num_edges", num_edges),
355        ("num_required_edges", num_required_edges),
356        ("num_vertices", num_vertices),
357    ];
358
359    fn variant() -> Vec<(&'static str, &'static str)> {
360        crate::variant_params![G, W]
361    }
362
363    fn evaluate(
364        &self,
365        config: &Self::Solution,
366    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
367        if config.len() != self.graph.num_edges() {
368            return Err(crate::traits::EvaluationError::InvalidConfiguration(
369                "edge-multiplicity vector length does not match the graph".into(),
370            ));
371        }
372        Ok(Min(self.is_valid_solution(config)?))
373    }
374}
375
376impl<G, W> crate::solvers::BruteForceProblem for RuralPostman<G, W>
377where
378    G: Graph + crate::variant::VariantParam,
379    W: WeightElement + crate::variant::VariantParam,
380{
381    fn dimensions(&self) -> Vec<usize> {
382        vec![3; self.graph.num_edges()]
383    }
384}
385
386crate::declare_variants! {
387    default RuralPostman<SimpleGraph, i64> => "2^num_vertices * num_vertices^2" create RuralPostmanCreateSpec,
388}
389
390crate::register_brute_force! {
391    RuralPostman<SimpleGraph, i64>,
392}
393
394#[cfg(feature = "example-db")]
395pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
396    use crate::topology::SimpleGraph;
397    // Issue #248 instance 1: hexagonal graph, 8 edges, E'={e0,e2,e4}
398    // Solution: hexagon cycle with all 6 unit-cost edges, config [1,1,1,1,1,1,0,0], cost=6
399    let graph = SimpleGraph::new(
400        6,
401        vec![
402            (0, 1),
403            (1, 2),
404            (2, 3),
405            (3, 4),
406            (4, 5),
407            (5, 0),
408            (0, 3),
409            (1, 4),
410        ],
411    );
412    vec![crate::example_db::specs::ModelExampleSpec {
413        id: "rural_postman",
414        instance: Box::new(RuralPostman::new(
415            graph,
416            vec![1, 1, 1, 1, 1, 1, 2, 2],
417            vec![0, 2, 4],
418        )),
419        optimal_config: serde_json::json!(vec![1, 1, 1, 1, 1, 1, 0, 0]),
420        optimal_value: serde_json::json!(6),
421    }]
422}
423
424#[cfg(test)]
425#[path = "../../unit_tests/models/graph/rural_postman.rs"]
426mod tests;
427
428crate::decision_problem_meta!(RuralPostman<SimpleGraph, i64>, "DecisionRuralPostman");
429crate::register_decision_variant!(
430    RuralPostman<SimpleGraph, i64>, "DecisionRuralPostman", "2^num_vertices * num_vertices^2", &[],
431    "Does a feasible solution have objective value <= the bound?",
432    category: crate::registry::ProblemCategory::Graph,
433    dims: [
434            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
435            VariantDimension::new("weight", "i64", &["i64"]),
436        ],
437    fields: [
438        crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Graph edges as comma-separated vertex pairs." },
439        crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices, including isolated vertices." },
440        crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec<i64>", description: "Weights for each edge in graph order." },
441        crate::registry::FieldInfo { name: "required_edges", type_name: "Vec<usize>", description: "Indices of edges that the route must traverse." },
442        crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values <= this bound" },
443    ],
444    decode: |_, indices: Vec<usize>| indices
445);
446
447#[cfg(feature = "example-db")]
448pub(crate) fn decision_canonical_rule_example_specs(
449) -> Vec<crate::example_db::specs::RuleExampleSpec> {
450    vec![crate::example_db::specs::RuleExampleSpec {
451        id: "decision_rural_postman_to_rural_postman",
452        build: || {
453            let graph = SimpleGraph::new(
454                6,
455                vec![
456                    (0, 1),
457                    (1, 2),
458                    (2, 3),
459                    (3, 4),
460                    (4, 5),
461                    (5, 0),
462                    (0, 3),
463                    (1, 4),
464                ],
465            );
466            let source = crate::models::decision::Decision::new(
467                RuralPostman::new(graph, vec![1, 1, 1, 1, 1, 1, 2, 2], vec![0, 2, 4]),
468                6,
469            );
470            let witness = serde_json::json!(vec![1, 1, 1, 1, 1, 1, 0, 0]);
471            crate::example_db::specs::rule_example_with_witness::<_, RuralPostman<SimpleGraph, i64>>(
472                source,
473                crate::export::SolutionPair {
474                    source_config: witness.clone(),
475                    target_config: witness,
476                },
477            )
478        },
479    }]
480}