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