1use crate::registry::{CreateSpec, 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", "i64", &["i64", "One"]),
24 ],
25 category: crate::registry::ProblemCategory::Graph,
26 module_path: module_path!(),
27 description: "Find a minimum-cost closed walk covering all arcs and edges in a mixed graph",
28 fields: MixedChinesePostmanI64CreateSpec::FIELDS,
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct MixedChinesePostman<W: WeightElement<Sum = i64>> {
40 graph: MixedGraph,
41 arc_weights: Vec<W>,
42 edge_weights: Vec<W>,
43}
44
45macro_rules! mixed_chinese_postman_create_spec {
46 ($name:ident, $weight:ty, $one:expr $(, $arc_weights:ident, $edge_weights:ident)?) => {
47 #[derive(Debug, Deserialize, crate::CreateSpec)]
48 struct $name {
49 #[create(codec = "edge-list")]
51 graph: Vec<(usize, usize)>,
52 #[create(codec = "arc-list")]
54 arcs: Vec<(usize, usize)>,
55 num_vertices: Option<usize>,
57 $(
58 #[create(codec = "comma-separated")]
60 $arc_weights: Option<Vec<$weight>>,
61 )?
62 $(
63 #[create(codec = "comma-separated")]
65 $edge_weights: Option<Vec<$weight>>,
66 )?
67 }
68
69 impl TryFrom<$name> for MixedChinesePostman<$weight> {
70 type Error = crate::registry::ConstructionError;
71
72 fn try_from(spec: $name) -> Result<Self, Self::Error> {
73 if spec.graph.is_empty() && spec.num_vertices.is_none() {
74 return Err("num_vertices is required for an empty graph".to_string().into());
75 }
76 if spec.arcs.is_empty() {
77 return Err("arcs must be non-empty".to_string().into());
78 }
79 for (index, &(u, v)) in spec.graph.iter().enumerate() {
80 if u == v {
81 return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
82 }
83 }
84 let inferred = spec
85 .graph
86 .iter()
87 .flat_map(|&(u, v)| [u, v])
88 .max()
89 .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
90 .transpose()?
91 .unwrap_or(0);
92 let num_vertices = spec.num_vertices.unwrap_or(inferred);
93 if num_vertices < inferred {
94 return Err(format!(
95 "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
96 ).into());
97 }
98 for (index, &(u, v)) in spec.arcs.iter().enumerate() {
99 if u >= num_vertices || v >= num_vertices {
100 return Err(format!(
101 "arc {index} endpoint is out of range for {num_vertices} vertices"
102 ).into());
103 }
104 }
105 let arc_weights = { $(if let Some(value) = spec.$arc_weights { value } else)? { vec![$one; spec.arcs.len()] } };
106 let edge_weights = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; spec.graph.len()] } };
107 MixedChinesePostman::try_new(
108 MixedGraph::new(num_vertices, spec.arcs, spec.graph),
109 arc_weights,
110 edge_weights,
111 )
112 }
113 }
114 };
115}
116
117mixed_chinese_postman_create_spec!(
118 MixedChinesePostmanI64CreateSpec,
119 i64,
120 1_i64,
121 arc_weights,
122 edge_weights
123);
124mixed_chinese_postman_create_spec!(MixedChinesePostmanOneCreateSpec, One, One);
125
126impl<W: WeightElement<Sum = i64>> MixedChinesePostman<W> {
127 pub fn new(graph: MixedGraph, arc_weights: Vec<W>, edge_weights: Vec<W>) -> Self {
134 Self::try_new(graph, arc_weights, edge_weights)
135 .unwrap_or_else(|message| panic!("{message}"))
136 }
137
138 pub fn try_new(
140 graph: MixedGraph,
141 arc_weights: Vec<W>,
142 edge_weights: Vec<W>,
143 ) -> Result<Self, crate::registry::ConstructionError> {
144 if arc_weights.len() != graph.num_arcs() {
145 return Err("arc_weights length must match num_arcs".to_string().into());
146 }
147 if edge_weights.len() != graph.num_edges() {
148 return Err("edge_weights length must match num_edges"
149 .to_string()
150 .into());
151 }
152 for (index, weight) in arc_weights.iter().enumerate() {
153 if !matches!(
154 weight.to_sum().partial_cmp(&W::Sum::zero()),
155 Some(Ordering::Equal | Ordering::Greater)
156 ) {
157 return Err(format!("arc weight at index {index} must be nonnegative").into());
158 }
159 }
160 for (index, weight) in edge_weights.iter().enumerate() {
161 if !matches!(
162 weight.to_sum().partial_cmp(&W::Sum::zero()),
163 Some(Ordering::Equal | Ordering::Greater)
164 ) {
165 return Err(format!("edge weight at index {index} must be nonnegative").into());
166 }
167 }
168
169 Ok(Self {
170 graph,
171 arc_weights,
172 edge_weights,
173 })
174 }
175
176 pub fn graph(&self) -> &MixedGraph {
178 &self.graph
179 }
180
181 pub fn arc_weights(&self) -> &[W] {
183 &self.arc_weights
184 }
185
186 pub fn edge_weights(&self) -> &[W] {
188 &self.edge_weights
189 }
190
191 pub fn num_vertices(&self) -> usize {
193 self.graph.num_vertices()
194 }
195
196 pub fn num_arcs(&self) -> usize {
198 self.graph.num_arcs()
199 }
200
201 pub fn num_edges(&self) -> usize {
203 self.graph.num_edges()
204 }
205
206 pub fn is_weighted(&self) -> bool {
208 !W::IS_UNIT
209 }
210
211 fn oriented_arc_pairs(&self, config: &[bool]) -> Option<Vec<(usize, usize)>> {
212 if config.len() != self.graph.num_edges() {
213 return None;
214 }
215
216 let mut arcs = self.graph.arcs();
217 for ((u, v), &reverse) in self.graph.edges().iter().zip(config.iter()) {
218 if reverse {
219 arcs.push((*v, *u));
220 } else {
221 arcs.push((*u, *v));
222 }
223 }
224 Some(arcs)
225 }
226
227 fn available_arc_pairs(&self) -> Vec<(usize, usize)> {
228 let mut arcs = self.graph.arcs();
229 for &(u, v) in self.graph.edges().iter() {
230 arcs.push((u, v));
231 arcs.push((v, u));
232 }
233 arcs
234 }
235
236 fn weighted_available_arcs(&self) -> Vec<(usize, usize, i64)> {
237 let mut arcs: Vec<(usize, usize, i64)> = self
238 .graph
239 .arcs()
240 .into_iter()
241 .zip(self.arc_weights.iter())
242 .map(|((u, v), weight)| (u, v, weight.to_sum()))
243 .collect();
244
245 for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) {
246 let cost = weight.to_sum();
247 arcs.push((*u, *v, cost));
248 arcs.push((*v, *u, cost));
249 }
250
251 arcs
252 }
253
254 fn base_cost(&self) -> Result<i64, crate::traits::EvaluationError> {
255 let mut total = 0_i64;
256 for weight in self.arc_weights.iter().chain(self.edge_weights.iter()) {
257 total = W::checked_add_to_sum(
258 total,
259 weight.to_sum(),
260 "summing mixed Chinese postman base costs",
261 )?;
262 }
263 Ok(total)
264 }
265}
266
267impl<W> MixedChinesePostman<W>
268where
269 W: WeightElement<Sum = i64> + crate::variant::VariantParam,
270{
271 pub fn is_valid_solution(
274 &self,
275 config: &[bool],
276 ) -> Result<bool, crate::traits::EvaluationError> {
277 Ok(self.evaluate_solution(config)?.0.is_some())
278 }
279
280 fn evaluate_solution(
281 &self,
282 config: &[bool],
283 ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
284 if config.len() != self.graph.num_edges() {
285 return Err(crate::traits::EvaluationError::InvalidConfiguration(
286 "edge-orientation length does not match the undirected edges".into(),
287 ));
288 }
289 let Some(oriented_pairs) = self.oriented_arc_pairs(config) else {
290 return Ok(Min(None));
291 };
292
293 let available = self.available_arc_pairs();
294 let mut keep = vec![false; self.graph.num_vertices()];
295 for &(tail, head) in &available {
296 keep[tail] = true;
297 keep[head] = true;
298 }
299 if !DirectedGraph::new(self.graph.num_vertices(), available)
300 .induced_subgraph(&keep)
301 .is_strongly_connected()
302 {
303 return Ok(Min(None));
304 }
305
306 let distances =
307 all_pairs_shortest_paths(self.graph.num_vertices(), &self.weighted_available_arcs())?;
308 let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs)?;
309 let Some(extra_cost) = minimum_balancing_cost(&balance, &distances)? else {
310 return Ok(Min(None));
311 };
312
313 let total = self.base_cost()?.checked_add(extra_cost).ok_or_else(|| {
314 crate::traits::EvaluationError::IntegerOverflow(
315 "summing mixed Chinese postman objective".to_string(),
316 )
317 })?;
318 Ok(Min(Some(total)))
319 }
320}
321
322impl<W> Problem for MixedChinesePostman<W>
323where
324 W: WeightElement<Sum = i64> + crate::variant::VariantParam,
325{
326 const NAME: &'static str = "MixedChinesePostman";
327 type Solution = Vec<bool>;
328 type Value = Min<W::Sum>;
329
330 crate::problem_parameters![
331 ("num_arcs", num_arcs),
332 ("num_edges", num_edges),
333 ("num_vertices", num_vertices),
334 ];
335
336 fn variant() -> Vec<(&'static str, &'static str)> {
337 crate::variant_params![W]
338 }
339
340 fn evaluate(
341 &self,
342 config: &Self::Solution,
343 ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
344 self.evaluate_solution(config)
345 }
346}
347
348impl<W> crate::solvers::BruteForceProblem for MixedChinesePostman<W>
349where
350 W: WeightElement<Sum = i64> + crate::variant::VariantParam,
351{
352 fn dimensions(&self) -> Vec<usize> {
353 vec![2; self.graph.num_edges()]
354 }
355}
356
357crate::declare_variants! {
358 default MixedChinesePostman<i64> => "2^num_edges * num_vertices^3" create MixedChinesePostmanI64CreateSpec,
359 MixedChinesePostman<One> => "2^num_edges * num_vertices^3" create MixedChinesePostmanOneCreateSpec,
360}
361
362crate::register_brute_force! {
363 MixedChinesePostman<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
364 MixedChinesePostman<One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
365}
366
367#[cfg(feature = "example-db")]
368pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
369 vec![crate::example_db::specs::ModelExampleSpec {
370 id: "mixed_chinese_postman",
371 instance: Box::new(MixedChinesePostman::new(
372 MixedGraph::new(
373 5,
374 vec![(0, 1), (1, 2), (2, 3), (3, 0)],
375 vec![(0, 2), (1, 3), (0, 4), (4, 2)],
376 ),
377 vec![2, 3, 1, 4],
378 vec![2, 3, 1, 2],
379 )),
380 optimal_config: serde_json::json!(vec![true, true, false, false]),
381 optimal_value: serde_json::json!(21),
382 }]
383}
384
385fn all_pairs_shortest_paths(
386 num_vertices: usize,
387 arcs: &[(usize, usize, i64)],
388) -> Result<Vec<Vec<i64>>, crate::traits::EvaluationError> {
389 let mut distances = vec![vec![INF_COST; num_vertices]; num_vertices];
390
391 for (vertex, row) in distances.iter_mut().enumerate() {
392 row[vertex] = 0;
393 }
394
395 for &(u, v, cost) in arcs {
396 if cost < distances[u][v] {
397 distances[u][v] = cost;
398 }
399 }
400
401 for via in 0..num_vertices {
402 for src in 0..num_vertices {
403 if distances[src][via] == INF_COST {
404 continue;
405 }
406 for dst in 0..num_vertices {
407 if distances[via][dst] == INF_COST {
408 continue;
409 }
410 let through = distances[src][via]
411 .checked_add(distances[via][dst])
412 .ok_or_else(|| {
413 crate::traits::EvaluationError::IntegerOverflow(
414 "computing mixed Chinese postman shortest paths".to_string(),
415 )
416 })?;
417 if through < distances[src][dst] {
418 distances[src][dst] = through;
419 }
420 }
421 }
422 }
423
424 Ok(distances)
425}
426
427fn degree_imbalances(
428 num_vertices: usize,
429 arcs: &[(usize, usize)],
430) -> Result<Vec<i64>, crate::traits::EvaluationError> {
431 let mut balance = vec![0_i64; num_vertices];
432 for &(u, v) in arcs {
433 balance[u] = balance[u].checked_add(1).ok_or_else(|| {
434 crate::traits::EvaluationError::IntegerOverflow(
435 "computing mixed Chinese postman degree imbalance".to_string(),
436 )
437 })?;
438 balance[v] = balance[v].checked_sub(1).ok_or_else(|| {
439 crate::traits::EvaluationError::IntegerOverflow(
440 "computing mixed Chinese postman degree imbalance".to_string(),
441 )
442 })?;
443 }
444 Ok(balance)
445}
446
447fn minimum_balancing_cost(
448 balance: &[i64],
449 distances: &[Vec<i64>],
450) -> Result<Option<i64>, crate::traits::EvaluationError> {
451 let mut deficits = Vec::new();
452 let mut surpluses = Vec::new();
453
454 for (vertex, &value) in balance.iter().enumerate() {
455 if value < 0 {
456 for _ in 0..value.unsigned_abs() {
457 deficits.push(vertex);
458 }
459 } else if value > 0 {
460 for _ in 0..value.unsigned_abs() {
461 surpluses.push(vertex);
462 }
463 }
464 }
465
466 if deficits.len() != surpluses.len() {
467 return Ok(None);
468 }
469 if deficits.is_empty() {
470 return Ok(Some(0));
471 }
472
473 let mut costs = vec![vec![INF_COST; surpluses.len()]; deficits.len()];
474 for (row, &src) in deficits.iter().enumerate() {
475 for (col, &dst) in surpluses.iter().enumerate() {
476 costs[row][col] = distances[src][dst];
477 }
478 }
479
480 hungarian_min_cost(&costs)
481}
482
483fn hungarian_min_cost(costs: &[Vec<i64>]) -> Result<Option<i64>, crate::traits::EvaluationError> {
484 let size = costs.len();
485 if size == 0 {
486 return Ok(Some(0));
487 }
488 if costs.iter().any(|row| row.len() != size) {
489 return Ok(None);
490 }
491
492 let mut u = vec![0_i64; size + 1];
493 let mut v = vec![0_i64; size + 1];
494 let mut p = vec![0_usize; size + 1];
495 let mut way = vec![0_usize; size + 1];
496
497 for row in 1..=size {
498 p[0] = row;
499 let mut column0 = 0;
500 let mut minv = vec![INF_COST; size + 1];
501 let mut used = vec![false; size + 1];
502
503 loop {
504 used[column0] = true;
505 let row0 = p[column0];
506 let mut delta = INF_COST;
507 let mut column1 = 0;
508
509 for column in 1..=size {
510 if used[column] {
511 continue;
512 }
513
514 let current = costs[row0 - 1][column - 1]
515 .checked_sub(u[row0])
516 .and_then(|value| value.checked_sub(v[column]))
517 .ok_or_else(|| {
518 crate::traits::EvaluationError::IntegerOverflow(
519 "computing mixed Chinese postman assignment costs".to_string(),
520 )
521 })?;
522 if current < minv[column] {
523 minv[column] = current;
524 way[column] = column0;
525 }
526 if minv[column] < delta {
527 delta = minv[column];
528 column1 = column;
529 }
530 }
531
532 if delta == INF_COST {
533 return Ok(None);
534 }
535
536 for column in 0..=size {
537 if used[column] {
538 u[p[column]] = u[p[column]].checked_add(delta).ok_or_else(|| {
539 crate::traits::EvaluationError::IntegerOverflow(
540 "updating mixed Chinese postman assignment potentials".to_string(),
541 )
542 })?;
543 v[column] = v[column].checked_sub(delta).ok_or_else(|| {
544 crate::traits::EvaluationError::IntegerOverflow(
545 "updating mixed Chinese postman assignment potentials".to_string(),
546 )
547 })?;
548 } else {
549 minv[column] = minv[column].checked_sub(delta).ok_or_else(|| {
550 crate::traits::EvaluationError::IntegerOverflow(
551 "updating mixed Chinese postman reduced costs".to_string(),
552 )
553 })?;
554 }
555 }
556
557 column0 = column1;
558 if p[column0] == 0 {
559 break;
560 }
561 }
562
563 loop {
564 let column1 = way[column0];
565 p[column0] = p[column1];
566 column0 = column1;
567 if column0 == 0 {
568 break;
569 }
570 }
571 }
572
573 let mut assignment = vec![0_usize; size + 1];
574 for column in 1..=size {
575 assignment[p[column]] = column;
576 }
577
578 let mut total = 0_i64;
579 for row in 1..=size {
580 let cost = costs[row - 1][assignment[row] - 1];
581 if cost == INF_COST {
582 return Ok(None);
583 }
584 total = total.checked_add(cost).ok_or_else(|| {
585 crate::traits::EvaluationError::IntegerOverflow(
586 "summing mixed Chinese postman assignment costs".to_string(),
587 )
588 })?;
589 }
590 Ok(Some(total))
591}
592
593#[cfg(test)]
594#[path = "../../unit_tests/models/graph/mixed_chinese_postman.rs"]
595mod tests;