Skip to main content

problemreductions/rules/
steinertree_ilp.rs

1//! Exact Steiner-tree formulation for signed edge weights.
2//!
3//! Binary vertex selectors and rooted flows connect every selected vertex.
4//! Endpoint linking and |selected edges| = |selected vertices| - 1 then enforce
5//! a tree, independently of the objective's signs.
6
7use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
8use crate::models::graph::SteinerTree;
9use crate::reduction;
10use crate::rules::traits::{ReduceTo, ReductionResult};
11use crate::topology::{Graph, SimpleGraph};
12
13/// Binary layout: m edge selectors, n vertex selectors, then 2m flow arcs
14/// for each vertex other than the first terminal (in vertex-index order).
15#[derive(Debug, Clone)]
16pub struct ReductionSteinerTreeToILP {
17    target: ILP<bool>,
18    num_edges: usize,
19}
20
21impl ReductionResult for ReductionSteinerTreeToILP {
22    type Source = SteinerTree<SimpleGraph, i64>;
23    type Target = ILP<bool>;
24
25    fn target_problem(&self) -> &ILP<bool> {
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        if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?
34            .value
35            .is_none()
36        {
37            return Err(crate::rules::ExtractionError::invalid(
38                "target ILP assignment is infeasible",
39            ));
40        }
41        Ok(target_solution[..self.num_edges]
42            .iter()
43            .map(|&value| value == 1)
44            .collect())
45    }
46}
47
48#[reduction(
49    transform = exact {
50        num_vars = "num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)",
51        num_constraints = "num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1",
52    },
53    unavailable = {
54        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
55    }
56)]
57impl ReduceTo<ILP<bool>> for SteinerTree<SimpleGraph, i64> {
58    type Result = ReductionSteinerTreeToILP;
59
60    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
61        let n = self.num_vertices();
62        let m = self.num_edges();
63        let (num_vars, num_constraints) = tree_ilp_sizes(n, m, self.terminals().len())?;
64        // The source constructor requires at least two distinct terminals.
65        let root = self.terminals()[0];
66        let edges = self.graph().edges();
67        let vertex_var = |v: usize| m + v;
68        let flow_var = |commodity: usize, edge: usize, dir: usize| {
69            m + n + commodity * (2 * m) + 2 * edge + dir
70        };
71        let mut constraints = Vec::with_capacity(num_constraints);
72
73        for (e, &(u, v)) in edges.iter().enumerate() {
74            for endpoint in [u, v] {
75                constraints.push(LinearConstraint::le(
76                    vec![(e, 1), (vertex_var(endpoint), -1)],
77                    0,
78                ));
79            }
80        }
81        for &terminal in self.terminals() {
82            constraints.push(LinearConstraint::eq(vec![(vertex_var(terminal), 1)], 1));
83        }
84        let cardinality = (0..m)
85            .map(|e| (e, 1))
86            .chain((0..n).map(|v| (vertex_var(v), -1)))
87            .collect();
88        constraints.push(LinearConstraint::eq(cardinality, -1));
89
90        for (commodity, sink) in (0..n).filter(|&v| v != root).enumerate() {
91            for vertex in 0..n {
92                let mut terms = Vec::new();
93                for (edge, &(u, v)) in edges.iter().enumerate() {
94                    if vertex == u {
95                        terms.push((flow_var(commodity, edge, 0), -1));
96                        terms.push((flow_var(commodity, edge, 1), 1));
97                    }
98                    if vertex == v {
99                        terms.push((flow_var(commodity, edge, 0), 1));
100                        terms.push((flow_var(commodity, edge, 1), -1));
101                    }
102                }
103                // Inflow - outflow = z_sink at sink and -z_sink at root.
104                if vertex == root {
105                    terms.push((vertex_var(sink), 1));
106                } else if vertex == sink {
107                    terms.push((vertex_var(sink), -1));
108                }
109                constraints.push(LinearConstraint::eq(terms, 0));
110            }
111            for edge in 0..m {
112                for dir in 0..2 {
113                    constraints.push(LinearConstraint::le(
114                        vec![(flow_var(commodity, edge, dir), 1), (edge, -1)],
115                        0,
116                    ));
117                }
118            }
119        }
120        let objective = self
121            .edge_weights()
122            .iter()
123            .enumerate()
124            .map(|(e, &w)| (e, w))
125            .collect();
126        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
127            .map_err(Self::target_construction)?;
128        Ok(ReductionSteinerTreeToILP {
129            target,
130            num_edges: m,
131        })
132    }
133}
134
135/// Bounds for all offsets and allocation sizes; n >= 2 is a source invariant.
136fn tree_ilp_sizes(
137    n: usize,
138    m: usize,
139    k: usize,
140) -> Result<(usize, usize), crate::rules::ReductionError> {
141    let overflow = || {
142        crate::rules::ReductionError::integer_overflow::<SteinerTree<SimpleGraph, i64>, ILP<bool>>(
143            "counting Steiner tree ILP variables and constraints",
144        )
145    };
146    let non_root = n.checked_sub(1).ok_or_else(overflow)?;
147    let arcs = m.checked_mul(2).ok_or_else(overflow)?;
148    let vars = arcs
149        .checked_mul(non_root)
150        .and_then(|x| x.checked_add(m))
151        .and_then(|x| x.checked_add(n))
152        .ok_or_else(overflow)?;
153    let rows = n
154        .checked_mul(non_root)
155        .and_then(|x| arcs.checked_mul(n).and_then(|a| x.checked_add(a)))
156        .and_then(|x| x.checked_add(k))
157        .and_then(|x| x.checked_add(1))
158        .ok_or_else(overflow)?;
159    Ok((vars, rows))
160}
161
162#[cfg(feature = "example-db")]
163pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
164    vec![crate::example_db::specs::RuleExampleSpec {
165        id: "steinertree_to_ilp",
166        build: || {
167            let source = SteinerTree::new(
168                SimpleGraph::new(
169                    5,
170                    vec![(0, 1), (1, 2), (1, 3), (3, 4), (0, 3), (3, 2), (2, 4)],
171                ),
172                vec![2, 2, 1, 1, 5, 5, 6],
173                vec![0, 2, 4],
174            );
175            crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
176        },
177    }]
178}
179
180#[cfg(test)]
181#[path = "../unit_tests/rules/steinertree_ilp.rs"]
182mod tests;