Skip to main content

problemreductions/rules/
mixedchinesepostman_ilp.rs

1//! Reduction from MixedChinesePostman to ILP.
2//!
3//! Choose an orientation for every undirected edge, then add integer traversal
4//! variables on available directed arcs to balance the oriented multigraph
5//! within the length bound. Uses connectivity flow constraints on both
6//! forward and reverse directions.
7
8use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
9use crate::models::graph::MixedChinesePostman;
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use crate::types::WeightElement;
13
14/// Result of reducing MixedChinesePostman to ILP.
15#[derive(Debug, Clone)]
16pub struct ReductionMCPToILP {
17    target: ILP<i64, i64, Bounded>,
18    num_undirected_edges: usize,
19}
20
21impl ReductionResult for ReductionMCPToILP {
22    type Source = MixedChinesePostman<i64>;
23    type Target = ILP<i64, i64, Bounded>;
24
25    fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
26        &self.target
27    }
28
29    fn extract_solution(
30        &self,
31        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
32    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
33        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
34
35        Ok({
36            // Return the orientation bits d_k in source edge order
37            target_solution[..self.num_undirected_edges]
38                .iter()
39                .map(|&value| value == 1)
40                .collect()
41        })
42    }
43}
44
45#[reduction(transform = upper_bound {
46    max_constraint_magnitude_bits = "(num_arcs + num_edges + 1) * (num_vertices + 1) + 1",
47    num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1",
48    num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2",
49    num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)",
50})]
51impl ReduceTo<ILP<i64, i64, Bounded>> for MixedChinesePostman<i64> {
52    type Result = ReductionMCPToILP;
53
54    #[allow(clippy::needless_range_loop)]
55    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
56        let n = self.num_vertices();
57        let m = self.num_arcs(); // original directed arcs
58        let q = self.num_edges(); // undirected edges
59        let r_count = m + q; // required traversals
60
61        // If R = 0, empty walk is feasible
62        if r_count == 0 {
63            return Ok(ReductionMCPToILP {
64                target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
65                    .map_err(Self::target_construction)?,
66                num_undirected_edges: 0,
67            });
68        }
69
70        // Available arc list A*: L = m + 2q arcs
71        // b_i = a_i for 0 <= i < m
72        // b_{m+2k} = (u_k, v_k), b_{m+2k+1} = (v_k, u_k)
73        let original_arcs = self.graph().arcs();
74        let undirected_edges = self.graph().edges();
75
76        let l = m + 2 * q; // total available arcs
77
78        // Build available arc list with lengths
79        let mut avail_arcs: Vec<(usize, usize)> = Vec::with_capacity(l);
80        let mut avail_lengths: Vec<i64> = Vec::with_capacity(l);
81
82        for (i, &(u, v)) in original_arcs.iter().enumerate() {
83            avail_arcs.push((u, v));
84            avail_lengths.push(self.arc_weights()[i].to_sum());
85        }
86        for (k, &(u, v)) in undirected_edges.iter().enumerate() {
87            let length = self.edge_weights()[k].to_sum();
88            avail_arcs.push((u, v)); // forward
89            avail_lengths.push(length);
90            avail_arcs.push((v, u)); // reverse
91            avail_lengths.push(length);
92        }
93
94        // Variable layout (from paper):
95        // d_k: index k (0..q) -- orientation bit
96        // g_j: index q + j (0..L) -- extra traversals
97        // y_j: index q + L + j -- binary use indicator
98        // z_v: index q + 2L + v -- binary vertex activity
99        // rho_v: index q + 2L + n + v -- root selector
100        // s: index q + 2L + 2n -- count of active vertices
101        // b_v: index q + 2L + 2n + 1 + v -- product s*rho_v
102        // f_j: index q + 2L + 3n + 1 + j -- forward connectivity flow
103        // h_j: index q + 3L + 3n + 1 + j -- reverse connectivity flow
104
105        let d_idx = |k: usize| k;
106        let g_idx = |j: usize| q + j;
107        let y_idx = |j: usize| q + l + j;
108        let z_idx = |v: usize| q + 2 * l + v;
109        let rho_idx = |v: usize| q + 2 * l + n + v;
110        let s_idx = q + 2 * l + 2 * n;
111        let b_idx = |v: usize| q + 2 * l + 2 * n + 1 + v;
112        let f_idx = |j: usize| q + 2 * l + 3 * n + 1 + j;
113        let h_idx = |j: usize| q + 3 * l + 3 * n + 1 + j;
114
115        let num_vars = q + 4 * l + 3 * n + 1;
116        let n_i64 = Self::exact_i64(n, "encoding the active-vertex count")?;
117        let r_count_i64 = Self::exact_i64(r_count, "encoding the required-arc count")?;
118        let big_g = r_count_i64.checked_mul(n_i64 - 1).ok_or_else(|| {
119            crate::rules::ReductionError::integer_overflow::<
120                MixedChinesePostman<i64>,
121                ILP<i64, i64, Bounded>,
122            >("computing the extra-traversal bound")
123        })?;
124        let m_use = big_g.checked_add(1).ok_or_else(|| {
125            crate::rules::ReductionError::integer_overflow::<
126                MixedChinesePostman<i64>,
127                ILP<i64, i64, Bounded>,
128            >("computing the arc-use bound")
129        })?;
130
131        let mut constraints = Vec::new();
132
133        // Binary bounds for d_k: 0 <= d_k <= 1
134        for k in 0..q {
135            constraints.push(LinearConstraint::le(vec![(d_idx(k), 1)], 1));
136        }
137
138        // Bounds on g_j: 0 <= g_j <= G
139        for j in 0..l {
140            constraints.push(LinearConstraint::le(vec![(g_idx(j), 1)], big_g));
141        }
142
143        // Binary bounds: y_j, z_v, rho_v <= 1
144        for j in 0..l {
145            constraints.push(LinearConstraint::le(vec![(y_idx(j), 1)], 1));
146        }
147        for v in 0..n {
148            constraints.push(LinearConstraint::le(vec![(z_idx(v), 1)], 1));
149            constraints.push(LinearConstraint::le(vec![(rho_idx(v), 1)], 1));
150        }
151
152        // The required multiplicity r_j(d):
153        // For original arcs (0 <= j < m): r_j = 1 (constant)
154        // For edge k forward (j = m + 2k): r_j = 1 - d_k
155        // For edge k reverse (j = m + 2k + 1): r_j = d_k
156
157        // Balance constraints:
158        // sum_{j: tail_j = v} (r_j + g_j) - sum_{j: head_j = v} (r_j + g_j) = 0 for all v
159        for v in 0..n {
160            let mut terms = Vec::new();
161            let mut constant = 0_i64; // constant part of r_j
162
163            for j in 0..l {
164                let (tail, head) = avail_arcs[j];
165                let sign = if tail == v && head == v {
166                    0 // self-loop contributes nothing
167                } else if tail == v {
168                    1
169                } else if head == v {
170                    -1
171                } else {
172                    continue;
173                };
174                if sign == 0 {
175                    continue;
176                }
177
178                // g_j term
179                terms.push((g_idx(j), sign));
180
181                // r_j term
182                if j < m {
183                    // Original arc: r_j = 1
184                    constant += sign;
185                } else {
186                    let k = (j - m) / 2;
187                    if (j - m).is_multiple_of(2) {
188                        // Forward: r_j = 1 - d_k => constant += sign, d_k term += -sign
189                        constant += sign;
190                        terms.push((d_idx(k), -sign));
191                    } else {
192                        // Reverse: r_j = d_k => d_k term += sign
193                        terms.push((d_idx(k), sign));
194                    }
195                }
196            }
197            // terms = -constant => 0
198            constraints.push(LinearConstraint::eq(terms, -constant));
199        }
200
201        // Use indicator: r_j + g_j <= M_use * y_j and y_j <= r_j + g_j
202        for j in 0..l {
203            if j < m {
204                // r_j = 1: (1 + g_j) <= M_use * y_j => g_j - M_use * y_j <= -1
205                constraints.push(LinearConstraint::le(
206                    vec![(g_idx(j), 1), (y_idx(j), -m_use)],
207                    -1,
208                ));
209                // y_j <= 1 + g_j => y_j - g_j <= 1
210                constraints.push(LinearConstraint::le(vec![(y_idx(j), 1), (g_idx(j), -1)], 1));
211            } else {
212                let k = (j - m) / 2;
213                if (j - m).is_multiple_of(2) {
214                    // Forward: r_j = 1 - d_k
215                    // (1 - d_k + g_j) <= M_use * y_j => g_j - d_k - M_use * y_j <= -1
216                    constraints.push(LinearConstraint::le(
217                        vec![(g_idx(j), 1), (d_idx(k), -1), (y_idx(j), -m_use)],
218                        -1,
219                    ));
220                    // y_j <= 1 - d_k + g_j => y_j + d_k - g_j <= 1
221                    constraints.push(LinearConstraint::le(
222                        vec![(y_idx(j), 1), (d_idx(k), 1), (g_idx(j), -1)],
223                        1,
224                    ));
225                } else {
226                    // Reverse: r_j = d_k
227                    // (d_k + g_j) <= M_use * y_j => d_k + g_j - M_use * y_j <= 0
228                    constraints.push(LinearConstraint::le(
229                        vec![(d_idx(k), 1), (g_idx(j), 1), (y_idx(j), -m_use)],
230                        0,
231                    ));
232                    // y_j <= d_k + g_j => y_j - d_k - g_j <= 0
233                    constraints.push(LinearConstraint::le(
234                        vec![(y_idx(j), 1), (d_idx(k), -1), (g_idx(j), -1)],
235                        0,
236                    ));
237                }
238            }
239        }
240
241        // Arc-vertex linking: y_j <= z_{tail_j} and y_j <= z_{head_j}
242        for j in 0..l {
243            let (tail, head) = avail_arcs[j];
244            constraints.push(LinearConstraint::le(
245                vec![(y_idx(j), 1), (z_idx(tail), -1)],
246                0,
247            ));
248            constraints.push(LinearConstraint::le(
249                vec![(y_idx(j), 1), (z_idx(head), -1)],
250                0,
251            ));
252        }
253
254        // z_v <= sum_{j: tail_j=v or head_j=v} y_j
255        for v in 0..n {
256            let mut terms = vec![(z_idx(v), 1)];
257            for j in 0..l {
258                let (tail, head) = avail_arcs[j];
259                if tail == v || head == v {
260                    terms.push((y_idx(j), -1));
261                }
262            }
263            constraints.push(LinearConstraint::le(terms, 0));
264        }
265
266        // s = sum_v z_v
267        {
268            let mut terms = vec![(s_idx, -1)];
269            for v in 0..n {
270                terms.push((z_idx(v), 1));
271            }
272            constraints.push(LinearConstraint::eq(terms, 0));
273        }
274
275        // Root selection: sum_v rho_v = 1, rho_v <= z_v
276        {
277            let terms: Vec<(usize, i64)> = (0..n).map(|v| (rho_idx(v), 1)).collect();
278            constraints.push(LinearConstraint::eq(terms, 1));
279        }
280        for v in 0..n {
281            constraints.push(LinearConstraint::le(
282                vec![(rho_idx(v), 1), (z_idx(v), -1)],
283                0,
284            ));
285        }
286
287        // Product linearization: b_v = s * rho_v
288        // b_v <= s, b_v <= n * rho_v, b_v >= s - n*(1 - rho_v), b_v >= 0
289        for v in 0..n {
290            constraints.push(LinearConstraint::le(vec![(b_idx(v), 1), (s_idx, -1)], 0));
291            constraints.push(LinearConstraint::le(
292                vec![(b_idx(v), 1), (rho_idx(v), -n_i64)],
293                0,
294            ));
295            constraints.push(LinearConstraint::ge(
296                vec![(b_idx(v), 1), (s_idx, -1), (rho_idx(v), -n_i64)],
297                -n_i64,
298            ));
299            // b_v >= 0 is implied by `ILP<i64, i64, Bounded>` non-negativity
300        }
301
302        // Flow bounds: 0 <= f_j, h_j <= (n-1) * y_j
303        let flow_big_m = n_i64 - 1;
304        for j in 0..l {
305            constraints.push(LinearConstraint::le(
306                vec![(f_idx(j), 1), (y_idx(j), -flow_big_m)],
307                0,
308            ));
309            constraints.push(LinearConstraint::le(
310                vec![(h_idx(j), 1), (y_idx(j), -flow_big_m)],
311                0,
312            ));
313        }
314
315        // Forward flow conservation:
316        // sum_{j: tail_j=v} f_j - sum_{j: head_j=v} f_j = b_v - z_v for all v
317        for v in 0..n {
318            let mut terms = Vec::new();
319            for j in 0..l {
320                let (tail, head) = avail_arcs[j];
321                if tail == v {
322                    terms.push((f_idx(j), 1));
323                }
324                if head == v {
325                    terms.push((f_idx(j), -1));
326                }
327            }
328            terms.push((b_idx(v), -1));
329            terms.push((z_idx(v), 1));
330            constraints.push(LinearConstraint::eq(terms, 0));
331        }
332
333        // Reverse flow conservation:
334        // sum_{j: head_j=v} h_j - sum_{j: tail_j=v} h_j = b_v - z_v for all v
335        for v in 0..n {
336            let mut terms = Vec::new();
337            for j in 0..l {
338                let (tail, head) = avail_arcs[j];
339                if head == v {
340                    terms.push((h_idx(j), 1));
341                }
342                if tail == v {
343                    terms.push((h_idx(j), -1));
344                }
345            }
346            terms.push((b_idx(v), -1));
347            terms.push((z_idx(v), 1));
348            constraints.push(LinearConstraint::eq(terms, 0));
349        }
350
351        // Objective: minimize total walk length = sum_j l_j * (r_j + g_j)
352        // Expand r_j: for original arcs r_j = 1 (constant), for edge k fwd r_j = 1 - d_k,
353        // for edge k rev r_j = d_k.
354        // constant part moves out of the objective (ILP ignores additive constants).
355        let mut objective = Vec::new();
356        for j in 0..l {
357            let len_j = avail_lengths[j];
358            // g_j term
359            objective.push((g_idx(j), len_j));
360            // d_k terms from r_j
361            if j >= m {
362                let k = (j - m) / 2;
363                if (j - m).is_multiple_of(2) {
364                    // r_j = 1 - d_k => cost contribution -len_j * d_k (constant +len_j ignored)
365                    objective.push((d_idx(k), -len_j));
366                } else {
367                    // r_j = d_k => cost contribution +len_j * d_k
368                    objective.push((d_idx(k), len_j));
369                }
370            }
371        }
372
373        let mut variables = vec![IntegerVariable::binary(); num_vars];
374        variables[q..q + l]
375            .fill(IntegerVariable::new(Some(0), Some(big_g)).map_err(Self::target_construction)?);
376        variables[s_idx..q + 2 * l + 3 * n + 1]
377            .fill(IntegerVariable::new(Some(0), Some(n_i64)).map_err(Self::target_construction)?);
378        variables[q + 2 * l + 3 * n + 1..].fill(
379            IntegerVariable::new(Some(0), Some(flow_big_m)).map_err(Self::target_construction)?,
380        );
381
382        let target =
383            ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize)
384                .map_err(Self::target_construction)?;
385
386        Ok(ReductionMCPToILP {
387            target,
388            num_undirected_edges: q,
389        })
390    }
391}
392
393#[cfg(feature = "example-db")]
394pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
395    use crate::topology::MixedGraph;
396
397    vec![crate::example_db::specs::RuleExampleSpec {
398        id: "mixedchinesepostman_to_ilp",
399        build: || {
400            // Simple instance: 3 vertices, 1 arc, 2 edges
401            let source = MixedChinesePostman::new(
402                MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]),
403                vec![1],
404                vec![1, 1],
405            );
406            crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source)
407        },
408    }]
409}
410
411#[cfg(test)]
412#[path = "../unit_tests/rules/mixedchinesepostman_ilp.rs"]
413mod tests;