problemreductions/models/graph/
mixed_chinese_postman.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
8use crate::topology::{DirectedGraph, MixedGraph};
9use crate::traits::Problem;
10use crate::types::{Min, One, WeightElement};
11use num_traits::Zero;
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14
15const INF_COST: i64 = i64::MAX / 4;
16
17inventory::submit! {
18 ProblemSchemaEntry {
19 name: "MixedChinesePostman",
20 display_name: "Mixed Chinese Postman",
21 aliases: &["MCPP"],
22 dimensions: &[
23 VariantDimension::new("weight", "i32", &["i32", "One"]),
24 ],
25 module_path: module_path!(),
26 description: "Find a minimum-cost closed walk covering all arcs and edges in a mixed graph",
27 fields: &[
28 FieldInfo { name: "graph", type_name: "MixedGraph", description: "The mixed graph G=(V,A,E)" },
29 FieldInfo { name: "arc_weights", type_name: "Vec<W>", description: "Lengths for the directed arcs in A" },
30 FieldInfo { name: "edge_weights", type_name: "Vec<W>", description: "Lengths for the undirected edges in E" },
31 ],
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct MixedChinesePostman<W: WeightElement<Sum = i32>> {
43 graph: MixedGraph,
44 arc_weights: Vec<W>,
45 edge_weights: Vec<W>,
46}
47
48impl<W: WeightElement<Sum = i32>> MixedChinesePostman<W> {
49 pub fn new(graph: MixedGraph, arc_weights: Vec<W>, edge_weights: Vec<W>) -> Self {
56 assert_eq!(
57 arc_weights.len(),
58 graph.num_arcs(),
59 "arc_weights length must match num_arcs"
60 );
61 assert_eq!(
62 edge_weights.len(),
63 graph.num_edges(),
64 "edge_weights length must match num_edges"
65 );
66 for (index, weight) in arc_weights.iter().enumerate() {
67 assert!(
68 matches!(
69 weight.to_sum().partial_cmp(&W::Sum::zero()),
70 Some(Ordering::Equal | Ordering::Greater)
71 ),
72 "arc weight at index {} must be nonnegative",
73 index
74 );
75 }
76 for (index, weight) in edge_weights.iter().enumerate() {
77 assert!(
78 matches!(
79 weight.to_sum().partial_cmp(&W::Sum::zero()),
80 Some(Ordering::Equal | Ordering::Greater)
81 ),
82 "edge weight at index {} must be nonnegative",
83 index
84 );
85 }
86
87 Self {
88 graph,
89 arc_weights,
90 edge_weights,
91 }
92 }
93
94 pub fn graph(&self) -> &MixedGraph {
96 &self.graph
97 }
98
99 pub fn arc_weights(&self) -> &[W] {
101 &self.arc_weights
102 }
103
104 pub fn edge_weights(&self) -> &[W] {
106 &self.edge_weights
107 }
108
109 pub fn num_vertices(&self) -> usize {
111 self.graph.num_vertices()
112 }
113
114 pub fn num_arcs(&self) -> usize {
116 self.graph.num_arcs()
117 }
118
119 pub fn num_edges(&self) -> usize {
121 self.graph.num_edges()
122 }
123
124 pub fn is_weighted(&self) -> bool {
126 !W::IS_UNIT
127 }
128
129 fn oriented_arc_pairs(&self, config: &[usize]) -> Option<Vec<(usize, usize)>> {
130 if config.len() != self.graph.num_edges() {
131 return None;
132 }
133
134 let mut arcs = self.graph.arcs();
135 for ((u, v), &direction) in self.graph.edges().iter().zip(config.iter()) {
136 match direction {
137 0 => arcs.push((*u, *v)),
138 1 => arcs.push((*v, *u)),
139 _ => return None,
140 }
141 }
142 Some(arcs)
143 }
144
145 fn available_arc_pairs(&self) -> Vec<(usize, usize)> {
146 let mut arcs = self.graph.arcs();
147 for &(u, v) in self.graph.edges().iter() {
148 arcs.push((u, v));
149 arcs.push((v, u));
150 }
151 arcs
152 }
153
154 fn weighted_available_arcs(&self) -> Vec<(usize, usize, i64)> {
155 let mut arcs: Vec<(usize, usize, i64)> = self
156 .graph
157 .arcs()
158 .into_iter()
159 .zip(self.arc_weights.iter())
160 .map(|((u, v), weight)| (u, v, i64::from(weight.to_sum())))
161 .collect();
162
163 for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) {
164 let cost = i64::from(weight.to_sum());
165 arcs.push((*u, *v, cost));
166 arcs.push((*v, *u, cost));
167 }
168
169 arcs
170 }
171
172 fn base_cost(&self) -> i64 {
173 self.arc_weights
174 .iter()
175 .map(|weight| i64::from(weight.to_sum()))
176 .sum::<i64>()
177 + self
178 .edge_weights
179 .iter()
180 .map(|weight| i64::from(weight.to_sum()))
181 .sum::<i64>()
182 }
183}
184
185impl<W> MixedChinesePostman<W>
186where
187 W: WeightElement<Sum = i32> + crate::variant::VariantParam,
188{
189 pub fn is_valid_solution(&self, config: &[usize]) -> bool {
192 self.evaluate(config).0.is_some()
193 }
194}
195
196impl<W> Problem for MixedChinesePostman<W>
197where
198 W: WeightElement<Sum = i32> + crate::variant::VariantParam,
199{
200 const NAME: &'static str = "MixedChinesePostman";
201 type Value = Min<W::Sum>;
202
203 fn variant() -> Vec<(&'static str, &'static str)> {
204 crate::variant_params![W]
205 }
206
207 fn dims(&self) -> Vec<usize> {
208 vec![2; self.graph.num_edges()]
209 }
210
211 fn evaluate(&self, config: &[usize]) -> Min<W::Sum> {
212 let Some(oriented_pairs) = self.oriented_arc_pairs(config) else {
213 return Min(None);
214 };
215
216 if !DirectedGraph::new(self.graph.num_vertices(), self.available_arc_pairs())
219 .is_strongly_connected()
220 {
221 return Min(None);
222 }
223
224 let distances =
227 all_pairs_shortest_paths(self.graph.num_vertices(), &self.weighted_available_arcs());
228 let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs);
231 let Some(extra_cost) = minimum_balancing_cost(&balance, &distances) else {
232 return Min(None);
233 };
234
235 let total = self.base_cost() + extra_cost;
236 Min(Some(total as W::Sum))
237 }
238}
239
240crate::declare_variants! {
241 default MixedChinesePostman<i32> => "2^num_edges * num_vertices^3",
242 MixedChinesePostman<One> => "2^num_edges * num_vertices^3",
243}
244
245#[cfg(feature = "example-db")]
246pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
247 vec![crate::example_db::specs::ModelExampleSpec {
248 id: "mixed_chinese_postman_i32",
249 instance: Box::new(MixedChinesePostman::new(
250 MixedGraph::new(
251 5,
252 vec![(0, 1), (1, 2), (2, 3), (3, 0)],
253 vec![(0, 2), (1, 3), (0, 4), (4, 2)],
254 ),
255 vec![2, 3, 1, 4],
256 vec![2, 3, 1, 2],
257 )),
258 optimal_config: vec![1, 1, 0, 0],
259 optimal_value: serde_json::json!(21),
260 }]
261}
262
263fn all_pairs_shortest_paths(num_vertices: usize, arcs: &[(usize, usize, i64)]) -> Vec<Vec<i64>> {
264 let mut distances = vec![vec![INF_COST; num_vertices]; num_vertices];
265
266 for (vertex, row) in distances.iter_mut().enumerate() {
267 row[vertex] = 0;
268 }
269
270 for &(u, v, cost) in arcs {
271 if cost < distances[u][v] {
272 distances[u][v] = cost;
273 }
274 }
275
276 for via in 0..num_vertices {
277 for src in 0..num_vertices {
278 if distances[src][via] == INF_COST {
279 continue;
280 }
281 for dst in 0..num_vertices {
282 if distances[via][dst] == INF_COST {
283 continue;
284 }
285 let through = distances[src][via] + distances[via][dst];
286 if through < distances[src][dst] {
287 distances[src][dst] = through;
288 }
289 }
290 }
291 }
292
293 distances
294}
295
296fn degree_imbalances(num_vertices: usize, arcs: &[(usize, usize)]) -> Vec<i32> {
297 let mut balance = vec![0_i32; num_vertices];
298 for &(u, v) in arcs {
299 balance[u] += 1;
300 balance[v] -= 1;
301 }
302 balance
303}
304
305fn minimum_balancing_cost(balance: &[i32], distances: &[Vec<i64>]) -> Option<i64> {
306 let mut deficits = Vec::new();
307 let mut surpluses = Vec::new();
308
309 for (vertex, &value) in balance.iter().enumerate() {
310 if value < 0 {
311 for _ in 0..usize::try_from(-value).ok()? {
312 deficits.push(vertex);
313 }
314 } else if value > 0 {
315 for _ in 0..usize::try_from(value).ok()? {
316 surpluses.push(vertex);
317 }
318 }
319 }
320
321 if deficits.len() != surpluses.len() {
322 return None;
323 }
324 if deficits.is_empty() {
325 return Some(0);
326 }
327
328 let mut costs = vec![vec![INF_COST; surpluses.len()]; deficits.len()];
329 for (row, &src) in deficits.iter().enumerate() {
330 for (col, &dst) in surpluses.iter().enumerate() {
331 costs[row][col] = distances[src][dst];
332 }
333 }
334
335 hungarian_min_cost(&costs)
336}
337
338fn hungarian_min_cost(costs: &[Vec<i64>]) -> Option<i64> {
339 let size = costs.len();
340 if size == 0 {
341 return Some(0);
342 }
343 if costs.iter().any(|row| row.len() != size) {
344 return None;
345 }
346
347 let mut u = vec![0_i64; size + 1];
348 let mut v = vec![0_i64; size + 1];
349 let mut p = vec![0_usize; size + 1];
350 let mut way = vec![0_usize; size + 1];
351
352 for row in 1..=size {
353 p[0] = row;
354 let mut column0 = 0;
355 let mut minv = vec![INF_COST; size + 1];
356 let mut used = vec![false; size + 1];
357
358 loop {
359 used[column0] = true;
360 let row0 = p[column0];
361 let mut delta = INF_COST;
362 let mut column1 = 0;
363
364 for column in 1..=size {
365 if used[column] {
366 continue;
367 }
368
369 let current = costs[row0 - 1][column - 1] - u[row0] - v[column];
370 if current < minv[column] {
371 minv[column] = current;
372 way[column] = column0;
373 }
374 if minv[column] < delta {
375 delta = minv[column];
376 column1 = column;
377 }
378 }
379
380 if delta == INF_COST {
381 return None;
382 }
383
384 for column in 0..=size {
385 if used[column] {
386 u[p[column]] += delta;
387 v[column] -= delta;
388 } else {
389 minv[column] -= delta;
390 }
391 }
392
393 column0 = column1;
394 if p[column0] == 0 {
395 break;
396 }
397 }
398
399 loop {
400 let column1 = way[column0];
401 p[column0] = p[column1];
402 column0 = column1;
403 if column0 == 0 {
404 break;
405 }
406 }
407 }
408
409 let mut assignment = vec![0_usize; size + 1];
410 for column in 1..=size {
411 assignment[p[column]] = column;
412 }
413
414 let mut total = 0_i64;
415 for row in 1..=size {
416 let cost = costs[row - 1][assignment[row] - 1];
417 if cost == INF_COST {
418 return None;
419 }
420 total += cost;
421 }
422 Some(total)
423}
424
425#[cfg(test)]
426#[path = "../../unit_tests/models/graph/mixed_chinese_postman.rs"]
427mod tests;