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)]
56pub struct RuralPostman<G, W: WeightElement> {
57    /// The underlying graph.
58    graph: G,
59    /// Lengths for each edge (in edge index order).
60    edge_lengths: Vec<W>,
61    /// Indices of required edges (subset E' ⊆ E).
62    required_edges: Vec<usize>,
63}
64
65#[derive(Debug, Deserialize, crate::CreateSpec)]
66struct RuralPostmanCreateSpec {
67    #[create(codec = "edge-list")]
68    graph: Vec<(usize, usize)>,
69    num_vertices: Option<usize>,
70    #[create(codec = "comma-separated")]
71    edge_weights: Option<Vec<i64>>,
72    #[create(codec = "comma-separated")]
73    required_edges: Vec<usize>,
74}
75
76impl TryFrom<RuralPostmanCreateSpec> for RuralPostman<SimpleGraph, i64> {
77    type Error = crate::registry::ConstructionError;
78
79    fn try_from(spec: RuralPostmanCreateSpec) -> Result<Self, Self::Error> {
80        let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
81        let edge_lengths = spec
82            .edge_weights
83            .unwrap_or_else(|| vec![1; graph.num_edges()]);
84        if edge_lengths.len() != graph.num_edges() {
85            return Err(format!(
86                "edge_weights has length {}, expected {}",
87                edge_lengths.len(),
88                graph.num_edges()
89            )
90            .into());
91        }
92        if let Some(&edge) = spec
93            .required_edges
94            .iter()
95            .find(|&&edge| edge >= graph.num_edges())
96        {
97            return Err(format!("required edge index {edge} is out of bounds").into());
98        }
99        Ok(Self::new(graph, edge_lengths, spec.required_edges))
100    }
101}
102
103fn simple_graph_from_create(
104    edges: Vec<(usize, usize)>,
105    num_vertices: Option<usize>,
106) -> Result<SimpleGraph, crate::registry::ConstructionError> {
107    if edges.is_empty() && num_vertices.is_none() {
108        return Err("num_vertices is required for an empty graph"
109            .to_string()
110            .into());
111    }
112    for (index, &(u, v)) in edges.iter().enumerate() {
113        if u == v {
114            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
115        }
116    }
117    let inferred = edges
118        .iter()
119        .flat_map(|&(u, v)| [u, v])
120        .max()
121        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
122        .transpose()?
123        .unwrap_or(0);
124    let num_vertices = num_vertices.unwrap_or(inferred);
125    if num_vertices < inferred {
126        return Err(format!(
127            "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
128        )
129        .into());
130    }
131    Ok(SimpleGraph::new(num_vertices, edges))
132}
133
134impl<G: Graph, W: WeightElement> RuralPostman<G, W> {
135    /// Create a new RuralPostman problem.
136    ///
137    /// # Panics
138    /// Panics if edge_lengths length does not match graph edges,
139    /// or if any required edge index is out of bounds.
140    pub fn new(graph: G, edge_lengths: Vec<W>, required_edges: Vec<usize>) -> Self {
141        assert_eq!(
142            edge_lengths.len(),
143            graph.num_edges(),
144            "edge_lengths length must match num_edges"
145        );
146        for &idx in &required_edges {
147            assert!(
148                idx < graph.num_edges(),
149                "required edge index {} out of bounds (graph has {} edges)",
150                idx,
151                graph.num_edges()
152            );
153        }
154        Self {
155            graph,
156            edge_lengths,
157            required_edges,
158        }
159    }
160
161    /// Get a reference to the underlying graph.
162    pub fn graph(&self) -> &G {
163        &self.graph
164    }
165
166    /// Get the edge lengths.
167    pub fn edge_lengths(&self) -> &[W] {
168        &self.edge_lengths
169    }
170
171    /// Get the required edge indices.
172    pub fn required_edges(&self) -> &[usize] {
173        &self.required_edges
174    }
175
176    /// Get the number of vertices in the underlying graph.
177    pub fn num_vertices(&self) -> usize {
178        self.graph.num_vertices()
179    }
180
181    /// Get the number of edges in the underlying graph.
182    pub fn num_edges(&self) -> usize {
183        self.graph.num_edges()
184    }
185
186    /// Get the number of required edges.
187    pub fn num_required_edges(&self) -> usize {
188        self.required_edges.len()
189    }
190
191    /// Set new edge lengths.
192    pub fn set_weights(&mut self, weights: Vec<W>) {
193        assert_eq!(weights.len(), self.graph.num_edges());
194        self.edge_lengths = weights;
195    }
196
197    /// Get the edge lengths as a Vec.
198    pub fn weights(&self) -> Vec<W> {
199        self.edge_lengths.clone()
200    }
201
202    /// Check if the problem uses a non-unit weight type.
203    pub fn is_weighted(&self) -> bool {
204        !W::IS_UNIT
205    }
206
207    /// Check if a configuration represents a valid circuit covering all required edges.
208    /// Returns `Some(cost)` if valid, `None` otherwise.
209    ///
210    /// Each `config[i]` is the multiplicity (number of traversals) of edge `i`.
211    pub fn is_valid_solution(
212        &self,
213        config: &[usize],
214    ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
215        if config.len() != self.graph.num_edges() {
216            return Ok(None);
217        }
218
219        let edges = self.graph.edges();
220        let n = self.graph.num_vertices();
221
222        // Check all required edges are traversed at least once
223        for &req_idx in &self.required_edges {
224            if config[req_idx] == 0 {
225                return Ok(None);
226            }
227        }
228
229        // Compute degree of each vertex (sum of multiplicities of incident edges)
230        let mut degree = vec![0usize; n];
231        let mut has_edges = false;
232        for (idx, &mult) in config.iter().enumerate() {
233            if mult > 0 {
234                let (u, v) = edges[idx];
235                degree[u] += mult;
236                degree[v] += mult;
237                has_edges = true;
238            }
239        }
240
241        // No edges used: only valid if no required edges
242        if !has_edges {
243            if self.required_edges.is_empty() {
244                return Ok(Some(W::Sum::zero()));
245            } else {
246                return Ok(None);
247            }
248        }
249
250        // All vertices must have even degree (Eulerian condition)
251        for &d in &degree {
252            if d % 2 != 0 {
253                return Ok(None);
254            }
255        }
256
257        // Edges with multiplicity > 0 must form a connected subgraph
258        // (considering only vertices with degree > 0)
259        let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
260        let mut first_vertex = None;
261        for (idx, &mult) in config.iter().enumerate() {
262            if mult > 0 {
263                let (u, v) = edges[idx];
264                adj[u].push(v);
265                adj[v].push(u);
266                if first_vertex.is_none() {
267                    first_vertex = Some(u);
268                }
269            }
270        }
271
272        let first = match first_vertex {
273            Some(v) => v,
274            None => {
275                if self.required_edges.is_empty() {
276                    return Ok(Some(W::Sum::zero()));
277                } else {
278                    return Ok(None);
279                }
280            }
281        };
282
283        let mut visited = vec![false; n];
284        let mut queue = VecDeque::new();
285        visited[first] = true;
286        queue.push_back(first);
287
288        while let Some(node) = queue.pop_front() {
289            for &neighbor in &adj[node] {
290                if !visited[neighbor] {
291                    visited[neighbor] = true;
292                    queue.push_back(neighbor);
293                }
294            }
295        }
296
297        // All vertices with degree > 0 must be visited
298        for v in 0..n {
299            if degree[v] > 0 && !visited[v] {
300                return Ok(None);
301            }
302        }
303
304        // Compute total cost (sum of multiplicity × edge length)
305        let mut total = W::Sum::zero();
306        for (idx, &mult) in config.iter().enumerate() {
307            for _ in 0..mult {
308                total = W::checked_add_to_sum(
309                    total,
310                    self.edge_lengths[idx].to_sum(),
311                    "summing rural postman edge lengths",
312                )?;
313            }
314        }
315
316        Ok(Some(total))
317    }
318}
319
320impl<G, W> Problem for RuralPostman<G, W>
321where
322    G: Graph + crate::variant::VariantParam,
323    W: WeightElement + crate::variant::VariantParam,
324{
325    const NAME: &'static str = "RuralPostman";
326    type Solution = Vec<usize>;
327    type Value = Min<W::Sum>;
328
329    crate::problem_parameters![
330        ("num_edges", num_edges),
331        ("num_required_edges", num_required_edges),
332        ("num_vertices", num_vertices),
333    ];
334
335    fn variant() -> Vec<(&'static str, &'static str)> {
336        crate::variant_params![G, W]
337    }
338
339    fn evaluate(
340        &self,
341        config: &Self::Solution,
342    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
343        if config.len() != self.graph.num_edges() {
344            return Err(crate::traits::EvaluationError::InvalidConfiguration(
345                "edge-multiplicity vector length does not match the graph".into(),
346            ));
347        }
348        Ok(Min(self.is_valid_solution(config)?))
349    }
350}
351
352impl<G, W> crate::solvers::BruteForceProblem for RuralPostman<G, W>
353where
354    G: Graph + crate::variant::VariantParam,
355    W: WeightElement + crate::variant::VariantParam,
356{
357    fn dimensions(&self) -> Vec<usize> {
358        vec![3; self.graph.num_edges()]
359    }
360}
361
362crate::declare_variants! {
363    default RuralPostman<SimpleGraph, i64> => "2^num_vertices * num_vertices^2" create RuralPostmanCreateSpec,
364}
365
366crate::register_brute_force! {
367    RuralPostman<SimpleGraph, i64>,
368}
369
370#[cfg(feature = "example-db")]
371pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
372    use crate::topology::SimpleGraph;
373    // Issue #248 instance 1: hexagonal graph, 8 edges, E'={e0,e2,e4}
374    // Solution: hexagon cycle with all 6 unit-cost edges, config [1,1,1,1,1,1,0,0], cost=6
375    let graph = SimpleGraph::new(
376        6,
377        vec![
378            (0, 1),
379            (1, 2),
380            (2, 3),
381            (3, 4),
382            (4, 5),
383            (5, 0),
384            (0, 3),
385            (1, 4),
386        ],
387    );
388    vec![crate::example_db::specs::ModelExampleSpec {
389        id: "rural_postman",
390        instance: Box::new(RuralPostman::new(
391            graph,
392            vec![1, 1, 1, 1, 1, 1, 2, 2],
393            vec![0, 2, 4],
394        )),
395        optimal_config: serde_json::json!(vec![1, 1, 1, 1, 1, 1, 0, 0]),
396        optimal_value: serde_json::json!(6),
397    }]
398}
399
400#[cfg(test)]
401#[path = "../../unit_tests/models/graph/rural_postman.rs"]
402mod tests;