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::{FieldInfo, 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", "i32", &["i32"]),
22        ],
23        module_path: module_path!(),
24        description: "Find a minimum-cost circuit covering all required edges (Rural Postman Problem)",
25        fields: &[
26            FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
27            FieldInfo { name: "edge_weights", type_name: "Vec<W>", description: "Edge lengths l(e) for each e in E" },
28            FieldInfo { name: "required_edges", type_name: "Vec<usize>", description: "Edge indices of the required subset E' ⊆ E" },
29        ],
30    }
31}
32
33/// The Rural Postman problem.
34///
35/// Given a weighted graph G = (V, E) with edge lengths l(e) and
36/// a subset E' ⊆ E of required edges, find a minimum-cost circuit
37/// (closed walk) in G that includes each edge in E'.
38///
39/// # Representation
40///
41/// Each edge is assigned a multiplicity variable:
42/// - 0: edge is not traversed
43/// - 1: edge is traversed once
44/// - 2: edge is traversed twice
45///
46/// A valid circuit requires:
47/// - All required edges have multiplicity ≥ 1
48/// - All vertices have even degree (sum of multiplicities of incident edges)
49/// - Edges with multiplicity > 0 form a connected subgraph
50///
51/// Note: In an optimal RPP solution on undirected graphs, each edge is
52/// traversed at most twice, so multiplicity ∈ {0, 1, 2} is sufficient.
53///
54/// # Type Parameters
55///
56/// * `G` - The graph type (e.g., `SimpleGraph`)
57/// * `W` - The weight type for edge lengths (e.g., `i32`, `f64`)
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RuralPostman<G, W: WeightElement> {
60    /// The underlying graph.
61    graph: G,
62    /// Lengths for each edge (in edge index order).
63    edge_lengths: Vec<W>,
64    /// Indices of required edges (subset E' ⊆ E).
65    required_edges: Vec<usize>,
66}
67
68impl<G: Graph, W: WeightElement> RuralPostman<G, W> {
69    /// Create a new RuralPostman problem.
70    ///
71    /// # Panics
72    /// Panics if edge_lengths length does not match graph edges,
73    /// or if any required edge index is out of bounds.
74    pub fn new(graph: G, edge_lengths: Vec<W>, required_edges: Vec<usize>) -> Self {
75        assert_eq!(
76            edge_lengths.len(),
77            graph.num_edges(),
78            "edge_lengths length must match num_edges"
79        );
80        for &idx in &required_edges {
81            assert!(
82                idx < graph.num_edges(),
83                "required edge index {} out of bounds (graph has {} edges)",
84                idx,
85                graph.num_edges()
86            );
87        }
88        Self {
89            graph,
90            edge_lengths,
91            required_edges,
92        }
93    }
94
95    /// Get a reference to the underlying graph.
96    pub fn graph(&self) -> &G {
97        &self.graph
98    }
99
100    /// Get the edge lengths.
101    pub fn edge_lengths(&self) -> &[W] {
102        &self.edge_lengths
103    }
104
105    /// Get the required edge indices.
106    pub fn required_edges(&self) -> &[usize] {
107        &self.required_edges
108    }
109
110    /// Get the number of vertices in the underlying graph.
111    pub fn num_vertices(&self) -> usize {
112        self.graph.num_vertices()
113    }
114
115    /// Get the number of edges in the underlying graph.
116    pub fn num_edges(&self) -> usize {
117        self.graph.num_edges()
118    }
119
120    /// Get the number of required edges.
121    pub fn num_required_edges(&self) -> usize {
122        self.required_edges.len()
123    }
124
125    /// Set new edge lengths.
126    pub fn set_weights(&mut self, weights: Vec<W>) {
127        assert_eq!(weights.len(), self.graph.num_edges());
128        self.edge_lengths = weights;
129    }
130
131    /// Get the edge lengths as a Vec.
132    pub fn weights(&self) -> Vec<W> {
133        self.edge_lengths.clone()
134    }
135
136    /// Check if the problem uses a non-unit weight type.
137    pub fn is_weighted(&self) -> bool {
138        !W::IS_UNIT
139    }
140
141    /// Check if a configuration represents a valid circuit covering all required edges.
142    /// Returns `Some(cost)` if valid, `None` otherwise.
143    ///
144    /// Each `config[i]` is the multiplicity (number of traversals) of edge `i`.
145    pub fn is_valid_solution(&self, config: &[usize]) -> Option<W::Sum> {
146        if config.len() != self.graph.num_edges() {
147            return None;
148        }
149
150        let edges = self.graph.edges();
151        let n = self.graph.num_vertices();
152
153        // Check all required edges are traversed at least once
154        for &req_idx in &self.required_edges {
155            if config[req_idx] == 0 {
156                return None;
157            }
158        }
159
160        // Compute degree of each vertex (sum of multiplicities of incident edges)
161        let mut degree = vec![0usize; n];
162        let mut has_edges = false;
163        for (idx, &mult) in config.iter().enumerate() {
164            if mult > 0 {
165                let (u, v) = edges[idx];
166                degree[u] += mult;
167                degree[v] += mult;
168                has_edges = true;
169            }
170        }
171
172        // No edges used: only valid if no required edges
173        if !has_edges {
174            if self.required_edges.is_empty() {
175                return Some(W::Sum::zero());
176            } else {
177                return None;
178            }
179        }
180
181        // All vertices must have even degree (Eulerian condition)
182        for &d in &degree {
183            if d % 2 != 0 {
184                return None;
185            }
186        }
187
188        // Edges with multiplicity > 0 must form a connected subgraph
189        // (considering only vertices with degree > 0)
190        let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
191        let mut first_vertex = None;
192        for (idx, &mult) in config.iter().enumerate() {
193            if mult > 0 {
194                let (u, v) = edges[idx];
195                adj[u].push(v);
196                adj[v].push(u);
197                if first_vertex.is_none() {
198                    first_vertex = Some(u);
199                }
200            }
201        }
202
203        let first = match first_vertex {
204            Some(v) => v,
205            None => {
206                if self.required_edges.is_empty() {
207                    return Some(W::Sum::zero());
208                } else {
209                    return None;
210                }
211            }
212        };
213
214        let mut visited = vec![false; n];
215        let mut queue = VecDeque::new();
216        visited[first] = true;
217        queue.push_back(first);
218
219        while let Some(node) = queue.pop_front() {
220            for &neighbor in &adj[node] {
221                if !visited[neighbor] {
222                    visited[neighbor] = true;
223                    queue.push_back(neighbor);
224                }
225            }
226        }
227
228        // All vertices with degree > 0 must be visited
229        for v in 0..n {
230            if degree[v] > 0 && !visited[v] {
231                return None;
232            }
233        }
234
235        // Compute total cost (sum of multiplicity × edge length)
236        let mut total = W::Sum::zero();
237        for (idx, &mult) in config.iter().enumerate() {
238            for _ in 0..mult {
239                total += self.edge_lengths[idx].to_sum();
240            }
241        }
242
243        Some(total)
244    }
245}
246
247impl<G, W> Problem for RuralPostman<G, W>
248where
249    G: Graph + crate::variant::VariantParam,
250    W: WeightElement + crate::variant::VariantParam,
251{
252    const NAME: &'static str = "RuralPostman";
253    type Value = Min<W::Sum>;
254
255    fn variant() -> Vec<(&'static str, &'static str)> {
256        crate::variant_params![G, W]
257    }
258
259    fn dims(&self) -> Vec<usize> {
260        vec![3; self.graph.num_edges()]
261    }
262
263    fn evaluate(&self, config: &[usize]) -> Min<W::Sum> {
264        Min(self.is_valid_solution(config))
265    }
266}
267
268crate::declare_variants! {
269    default RuralPostman<SimpleGraph, i32> => "2^num_vertices * num_vertices^2",
270}
271
272#[cfg(feature = "example-db")]
273pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
274    use crate::topology::SimpleGraph;
275    // Issue #248 instance 1: hexagonal graph, 8 edges, E'={e0,e2,e4}
276    // Solution: hexagon cycle with all 6 unit-cost edges, config [1,1,1,1,1,1,0,0], cost=6
277    let graph = SimpleGraph::new(
278        6,
279        vec![
280            (0, 1),
281            (1, 2),
282            (2, 3),
283            (3, 4),
284            (4, 5),
285            (5, 0),
286            (0, 3),
287            (1, 4),
288        ],
289    );
290    vec![crate::example_db::specs::ModelExampleSpec {
291        id: "rural_postman",
292        instance: Box::new(RuralPostman::new(
293            graph,
294            vec![1, 1, 1, 1, 1, 1, 2, 2],
295            vec![0, 2, 4],
296        )),
297        optimal_config: vec![1, 1, 1, 1, 1, 1, 0, 0],
298        optimal_value: serde_json::json!(6),
299    }]
300}
301
302#[cfg(test)]
303#[path = "../../unit_tests/models/graph/rural_postman.rs"]
304mod tests;