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