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