Skip to main content

problemreductions/rules/
rootedtreestorageassignment_ilp.rs

1//! Reduction from RootedTreeStorageAssignment to ILP (Integer Linear Programming).
2//!
3//! Uses parent indicators p_{v,u}, depth variables d_v, ancestor indicators
4//! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets
5//! (top/bottom selectors, pair selectors, endpoint depths, extension costs).
6
7use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
8use crate::models::set::RootedTreeStorageAssignment;
9use crate::reduction;
10use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows};
11use crate::rules::traits::{ReduceTo, ReductionResult};
12
13// Index helpers
14
15fn idx_p(n: usize, v: usize, u: usize) -> usize {
16    v * n + u
17}
18
19fn idx_d(n: usize, v: usize) -> usize {
20    n * n + v
21}
22
23fn idx_a(n: usize, u: usize, v: usize) -> usize {
24    n * n + n + u * n + v
25}
26
27fn idx_h(n: usize, u: usize, v: usize, w: usize) -> usize {
28    2 * n * n + n + (u * n + v) * n + w
29}
30
31fn idx_t(n: usize, r: usize, s: usize, u: usize) -> usize {
32    let _ = r;
33    n * n * n + 2 * n * n + n + s * n + u
34}
35
36fn idx_b(n: usize, r: usize, s: usize, v: usize) -> usize {
37    n * n * n + 2 * n * n + n + r * n + s * n + v
38}
39
40fn idx_m(n: usize, r: usize, s: usize, u: usize, v: usize) -> usize {
41    n * n * n + 2 * n * n + n + 2 * r * n + s * n * n + u * n + v
42}
43
44fn idx_big_t(n: usize, r: usize, s: usize) -> usize {
45    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + s
46}
47
48fn idx_big_b(n: usize, r: usize, s: usize) -> usize {
49    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + r + s
50}
51
52fn idx_c(n: usize, r: usize, s: usize) -> usize {
53    n * n * n + 2 * n * n + n + 2 * r * n + r * n * n + 2 * r + s
54}
55
56fn total_vars(n: usize, r: usize) -> usize {
57    n * n * n + 2 * n * n + n + r * (n * n + 2 * n + 3)
58}
59
60#[derive(Debug, Clone)]
61pub struct ReductionRTSAToILP {
62    target: ILP<i64>,
63    n: usize,
64}
65
66impl ReductionResult for ReductionRTSAToILP {
67    type Source = RootedTreeStorageAssignment;
68    type Target = ILP<i64>;
69
70    fn target_problem(&self) -> &ILP<i64> {
71        &self.target
72    }
73
74    /// Decode parent array from one-hot parent indicators p_{v,u}.
75    fn extract_solution(
76        &self,
77        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
78    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
79        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
80
81        one_hot_decode_rows(target_solution, self.n, self.n, 0)
82    }
83}
84
85#[reduction(
86    transform = upper_bound {
87        num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)",
88        num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)",
89    },
90    unavailable = {
91        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
92    }
93)]
94impl ReduceTo<ILP<i64>> for RootedTreeStorageAssignment {
95    type Result = ReductionRTSAToILP;
96
97    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
98        let n = self.universe_size();
99        let subsets = self.subsets();
100        let bound = self.bound();
101
102        // Nontrivial subsets (size >= 2)
103        let nontrivial: Vec<usize> = (0..subsets.len())
104            .filter(|&k| subsets[k].len() >= 2)
105            .collect();
106        let r = nontrivial.len();
107
108        if n == 0 {
109            return Ok(ReductionRTSAToILP {
110                target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
111                    .map_err(Self::target_construction)?,
112                n,
113            });
114        }
115
116        let nv = total_vars(n, r);
117        let big_m = Self::exact_i64(n, "representing the vertex count in ILP rows")?;
118        let big_m_depth =
119            Self::exact_i64(n - 1, "representing the maximum tree depth in ILP rows")?;
120
121        let mut constraints = Vec::new();
122
123        // === Rooted-tree constraints ===
124
125        // Σ_u p_{v,u} = 1  ∀ v
126        for v in 0..n {
127            let terms: Vec<(usize, i64)> = (0..n).map(|u| (idx_p(n, v, u), 1)).collect();
128            constraints.push(LinearConstraint::eq(terms, 1));
129        }
130
131        // Σ_v p_{v,v} = 1 (exactly one root)
132        let root_terms: Vec<(usize, i64)> = (0..n).map(|v| (idx_p(n, v, v), 1)).collect();
133        constraints.push(LinearConstraint::eq(root_terms, 1));
134
135        // p_{v,u} binary: upper bound p_{v,u} <= 1
136        for v in 0..n {
137            for u in 0..n {
138                constraints.push(LinearConstraint::le(vec![(idx_p(n, v, u), 1)], 1));
139            }
140        }
141
142        // d_v <= (n-1)(1 - p_{v,v})  ∀ v  (root has depth 0)
143        for v in 0..n {
144            constraints.push(LinearConstraint::le(
145                vec![(idx_d(n, v), 1), (idx_p(n, v, v), big_m_depth)],
146                big_m_depth,
147            ));
148        }
149
150        // d_v >= 0  ∀ v
151        for v in 0..n {
152            constraints.push(LinearConstraint::ge(vec![(idx_d(n, v), 1)], 0));
153        }
154
155        // d_v <= n-1  ∀ v
156        for v in 0..n {
157            constraints.push(LinearConstraint::le(vec![(idx_d(n, v), 1)], big_m_depth));
158        }
159
160        // For u != v: d_v - d_u >= 1 - n(1 - p_{v,u})
161        //             d_v - d_u <= 1 + n(1 - p_{v,u})
162        for v in 0..n {
163            for u in 0..n {
164                if u != v {
165                    // d_v - d_u + n*p_{v,u} >= 1 - n + n = 1
166                    // => d_v - d_u + n*p_{v,u} >= 1 - n*(1 - p_{v,u})
167                    // Rewrite: d_v - d_u + n*p_{v,u} >= 1 - n + n*p_{v,u} ... no.
168                    // Original: d_v - d_u >= 1 - n(1 - p_{v,u})
169                    // => d_v - d_u + n - n*p_{v,u} >= 1
170                    // => d_v - d_u - n*p_{v,u} >= 1 - n
171                    constraints.push(LinearConstraint::ge(
172                        vec![
173                            (idx_d(n, v), 1),
174                            (idx_d(n, u), -1),
175                            (idx_p(n, v, u), -big_m),
176                        ],
177                        1 - big_m,
178                    ));
179
180                    // d_v - d_u <= 1 + n(1 - p_{v,u})
181                    // => d_v - d_u - n + n*p_{v,u} <= 1
182                    // => d_v - d_u + n*p_{v,u} <= 1 + n
183                    constraints.push(LinearConstraint::le(
184                        vec![(idx_d(n, v), 1), (idx_d(n, u), -1), (idx_p(n, v, u), big_m)],
185                        1 + big_m,
186                    ));
187                }
188            }
189        }
190
191        // === Ancestor relation ===
192
193        // a_{v,v} = 1  ∀ v
194        for v in 0..n {
195            constraints.push(LinearConstraint::eq(vec![(idx_a(n, v, v), 1)], 1));
196        }
197
198        // h_{u,v,v} = 0  ∀ u,v
199        for u in 0..n {
200            for v in 0..n {
201                constraints.push(LinearConstraint::eq(vec![(idx_h(n, u, v, v), 1)], 0));
202            }
203        }
204
205        // For u != v: a_{u,v} = Σ_w h_{u,v,w}
206        for u in 0..n {
207            for v in 0..n {
208                if u != v {
209                    let mut terms = vec![(idx_a(n, u, v), -1)];
210                    for w in 0..n {
211                        terms.push((idx_h(n, u, v, w), 1));
212                    }
213                    constraints.push(LinearConstraint::eq(terms, 0));
214                }
215            }
216        }
217
218        // h_{u,v,w} <= p_{v,w}  ∀ u,v,w with w != v
219        // h_{u,v,w} <= a_{u,w}  ∀ u,v,w with w != v
220        // h_{u,v,w} >= p_{v,w} + a_{u,w} - 1  ∀ u,v,w with w != v
221        for u in 0..n {
222            for v in 0..n {
223                for w in 0..n {
224                    if w != v {
225                        constraints.extend(mccormick_product(
226                            idx_h(n, u, v, w),
227                            idx_p(n, v, w),
228                            idx_a(n, u, w),
229                        ));
230                    }
231                }
232            }
233        }
234
235        // Binary bounds for a, h
236        for u in 0..n {
237            for v in 0..n {
238                constraints.push(LinearConstraint::le(vec![(idx_a(n, u, v), 1)], 1));
239                for w in 0..n {
240                    constraints.push(LinearConstraint::le(vec![(idx_h(n, u, v, w), 1)], 1));
241                }
242            }
243        }
244
245        // === Subset gadgets ===
246        for (s, &orig_k) in nontrivial.iter().enumerate() {
247            let subset = &subsets[orig_k];
248            let subset_size = subset.len();
249
250            // Top selectors: Σ_{u ∈ S} t_{s,u} = 1, t_{s,u} = 0 for u ∉ S
251            let top_terms: Vec<(usize, i64)> =
252                subset.iter().map(|&u| (idx_t(n, r, s, u), 1)).collect();
253            constraints.push(LinearConstraint::eq(top_terms, 1));
254            for u in 0..n {
255                if !subset.contains(&u) {
256                    constraints.push(LinearConstraint::eq(vec![(idx_t(n, r, s, u), 1)], 0));
257                }
258                // Binary bound
259                constraints.push(LinearConstraint::le(vec![(idx_t(n, r, s, u), 1)], 1));
260            }
261
262            // Bottom selectors: Σ_{v ∈ S} b_{s,v} = 1, b_{s,v} = 0 for v ∉ S
263            let bot_terms: Vec<(usize, i64)> =
264                subset.iter().map(|&v| (idx_b(n, r, s, v), 1)).collect();
265            constraints.push(LinearConstraint::eq(bot_terms, 1));
266            for v in 0..n {
267                if !subset.contains(&v) {
268                    constraints.push(LinearConstraint::eq(vec![(idx_b(n, r, s, v), 1)], 0));
269                }
270                constraints.push(LinearConstraint::le(vec![(idx_b(n, r, s, v), 1)], 1));
271            }
272
273            // Pair selectors (McCormick): m_{s,u,v} = t_{s,u} * b_{s,v}
274            for u in 0..n {
275                for v in 0..n {
276                    constraints.extend(mccormick_product(
277                        idx_m(n, r, s, u, v),
278                        idx_t(n, r, s, u),
279                        idx_b(n, r, s, v),
280                    ));
281                    constraints.push(LinearConstraint::le(vec![(idx_m(n, r, s, u, v), 1)], 1));
282                }
283            }
284
285            // Path condition: m_{s,u,v} <= a_{u,v} (top is ancestor of bottom)
286            for u in 0..n {
287                for v in 0..n {
288                    constraints.push(LinearConstraint::le(
289                        vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, u, v), -1)],
290                        0,
291                    ));
292                }
293            }
294
295            // Every subset element w lies on the chain:
296            // m_{s,u,v} <= a_{u,w} and m_{s,u,v} <= a_{w,v}  ∀ w ∈ S, u, v
297            for &w in subset {
298                for u in 0..n {
299                    for v in 0..n {
300                        constraints.push(LinearConstraint::le(
301                            vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, u, w), -1)],
302                            0,
303                        ));
304                        constraints.push(LinearConstraint::le(
305                            vec![(idx_m(n, r, s, u, v), 1), (idx_a(n, w, v), -1)],
306                            0,
307                        ));
308                    }
309                }
310            }
311
312            // Endpoint depths: T_s, B_s
313            // T_s - d_u <= (n-1)(1 - t_{s,u})  and  d_u - T_s <= (n-1)(1 - t_{s,u})
314            for &u in subset {
315                constraints.push(LinearConstraint::le(
316                    vec![
317                        (idx_big_t(n, r, s), 1),
318                        (idx_d(n, u), -1),
319                        (idx_t(n, r, s, u), big_m_depth),
320                    ],
321                    big_m_depth,
322                ));
323                constraints.push(LinearConstraint::le(
324                    vec![
325                        (idx_d(n, u), 1),
326                        (idx_big_t(n, r, s), -1),
327                        (idx_t(n, r, s, u), big_m_depth),
328                    ],
329                    big_m_depth,
330                ));
331            }
332            // B_s - d_v <= (n-1)(1 - b_{s,v})  and  d_v - B_s <= (n-1)(1 - b_{s,v})
333            for &v in subset {
334                constraints.push(LinearConstraint::le(
335                    vec![
336                        (idx_big_b(n, r, s), 1),
337                        (idx_d(n, v), -1),
338                        (idx_b(n, r, s, v), big_m_depth),
339                    ],
340                    big_m_depth,
341                ));
342                constraints.push(LinearConstraint::le(
343                    vec![
344                        (idx_d(n, v), 1),
345                        (idx_big_b(n, r, s), -1),
346                        (idx_b(n, r, s, v), big_m_depth),
347                    ],
348                    big_m_depth,
349                ));
350            }
351
352            // Depth bounds for T_s, B_s
353            constraints.push(LinearConstraint::ge(vec![(idx_big_t(n, r, s), 1)], 0));
354            constraints.push(LinearConstraint::le(
355                vec![(idx_big_t(n, r, s), 1)],
356                big_m_depth,
357            ));
358            constraints.push(LinearConstraint::ge(vec![(idx_big_b(n, r, s), 1)], 0));
359            constraints.push(LinearConstraint::le(
360                vec![(idx_big_b(n, r, s), 1)],
361                big_m_depth,
362            ));
363
364            // Extension cost: c_s = B_s - T_s + 1 - |S|
365            // => c_s - B_s + T_s = 1 - |S|
366            constraints.push(LinearConstraint::eq(
367                vec![
368                    (idx_c(n, r, s), 1),
369                    (idx_big_b(n, r, s), -1),
370                    (idx_big_t(n, r, s), 1),
371                ],
372                1 - Self::exact_i64(
373                    subset_size,
374                    "representing a subset cardinality in an ILP row",
375                )?,
376            ));
377
378            // c_s >= 0
379            constraints.push(LinearConstraint::ge(vec![(idx_c(n, r, s), 1)], 0));
380        }
381
382        // Total cost bound: Σ c_s <= K
383        if r > 0 {
384            let cost_terms: Vec<(usize, i64)> = (0..r).map(|s| (idx_c(n, r, s), 1)).collect();
385            constraints.push(LinearConstraint::le(cost_terms, bound));
386        }
387
388        let target = ILP::new(nv, constraints, vec![], ObjectiveSense::Minimize)
389            .map_err(Self::target_construction)?;
390        Ok(ReductionRTSAToILP { target, n })
391    }
392}
393
394#[cfg(feature = "example-db")]
395pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
396    use crate::export::SolutionPair;
397    vec![crate::example_db::specs::RuleExampleSpec {
398        id: "rootedtreestorageassignment_to_ilp",
399        build: || {
400            let source = RootedTreeStorageAssignment::new(3, vec![vec![0, 1], vec![1, 2]], 1);
401            let reduction: ReductionRTSAToILP =
402                ReduceTo::<ILP<i64>>::reduce_to(&source).expect("reduction should succeed");
403            let target_config = {
404                let ilp_solver = crate::solvers::ILPSolver::new();
405                ilp_solver
406                    .solve(reduction.target_problem())
407                    .expect("ILP should be solvable")
408            };
409            let source_config = reduction.extract_solution(&target_config).unwrap();
410            crate::example_db::specs::rule_example_with_witness::<_, ILP<i64>>(
411                source,
412                SolutionPair {
413                    source_config: serde_json::to_value(source_config)
414                        .expect("solution serialization must succeed"),
415                    target_config: serde_json::to_value(target_config)
416                        .expect("solution serialization must succeed"),
417                },
418            )
419        },
420    }]
421}
422
423#[cfg(test)]
424#[path = "../unit_tests/rules/rootedtreestorageassignment_ilp.rs"]
425mod tests;