problemreductions/rules/
steinertree_ilp.rs1use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
11use crate::models::graph::SteinerTree;
12use crate::reduction;
13use crate::rules::traits::{ReduceTo, ReductionResult};
14use crate::topology::{Graph, SimpleGraph};
15
16#[derive(Debug, Clone)]
23pub struct ReductionSteinerTreeToILP {
24 target: ILP<bool>,
25 num_edges: usize,
26}
27
28impl ReductionResult for ReductionSteinerTreeToILP {
29 type Source = SteinerTree<SimpleGraph, i32>;
30 type Target = ILP<bool>;
31
32 fn target_problem(&self) -> &ILP<bool> {
33 &self.target
34 }
35
36 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
37 target_solution[..self.num_edges].to_vec()
38 }
39}
40
41#[reduction(
42 overhead = {
43 num_vars = "num_edges + 2 * num_edges * (num_terminals - 1)",
44 num_constraints = "num_vertices * (num_terminals - 1) + 2 * num_edges * (num_terminals - 1)",
45 }
46)]
47impl ReduceTo<ILP<bool>> for SteinerTree<SimpleGraph, i32> {
48 type Result = ReductionSteinerTreeToILP;
49
50 fn reduce_to(&self) -> Self::Result {
51 assert!(
52 self.edge_weights().iter().all(|&weight| weight > 0),
53 "SteinerTree -> ILP requires strictly positive edge weights (zero-weight edges should be contracted beforehand)"
54 );
55
56 let n = self.num_vertices();
57 let m = self.num_edges();
58 let root = self.terminals()[0];
59 let non_root_terminals = &self.terminals()[1..];
60 let edges = self.graph().edges();
61 let num_vars = m + 2 * m * non_root_terminals.len();
62 let num_constraints = n * non_root_terminals.len() + 2 * m * non_root_terminals.len();
63 let mut constraints = Vec::with_capacity(num_constraints);
64
65 let edge_var = |edge_idx: usize| edge_idx;
66 let flow_var = |terminal_pos: usize, edge_idx: usize, dir: usize| -> usize {
67 m + terminal_pos * 2 * m + 2 * edge_idx + dir
68 };
69
70 for (terminal_pos, &terminal) in non_root_terminals.iter().enumerate() {
71 for vertex in 0..n {
72 let mut terms = Vec::new();
73 for (edge_idx, &(u, v)) in edges.iter().enumerate() {
74 if v == vertex {
75 terms.push((flow_var(terminal_pos, edge_idx, 0), 1.0));
76 terms.push((flow_var(terminal_pos, edge_idx, 1), -1.0));
77 }
78 if u == vertex {
79 terms.push((flow_var(terminal_pos, edge_idx, 0), -1.0));
80 terms.push((flow_var(terminal_pos, edge_idx, 1), 1.0));
81 }
82 }
83
84 let rhs = if vertex == root {
85 -1.0
86 } else if vertex == terminal {
87 1.0
88 } else {
89 0.0
90 };
91 constraints.push(LinearConstraint::eq(terms, rhs));
92 }
93 }
94
95 for terminal_pos in 0..non_root_terminals.len() {
96 for edge_idx in 0..m {
97 let selector = edge_var(edge_idx);
98 constraints.push(LinearConstraint::le(
99 vec![(flow_var(terminal_pos, edge_idx, 0), 1.0), (selector, -1.0)],
100 0.0,
101 ));
102 constraints.push(LinearConstraint::le(
103 vec![(flow_var(terminal_pos, edge_idx, 1), 1.0), (selector, -1.0)],
104 0.0,
105 ));
106 }
107 }
108
109 let objective: Vec<(usize, f64)> = self
110 .edge_weights()
111 .iter()
112 .enumerate()
113 .map(|(edge_idx, &weight)| (edge_var(edge_idx), weight as f64))
114 .collect();
115
116 let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize);
117
118 ReductionSteinerTreeToILP {
119 target,
120 num_edges: m,
121 }
122 }
123}
124
125#[cfg(feature = "example-db")]
126pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
127 vec![crate::example_db::specs::RuleExampleSpec {
128 id: "steinertree_to_ilp",
129 build: || {
130 let source = SteinerTree::new(
131 SimpleGraph::new(
132 5,
133 vec![(0, 1), (1, 2), (1, 3), (3, 4), (0, 3), (3, 2), (2, 4)],
134 ),
135 vec![2, 2, 1, 1, 5, 5, 6],
136 vec![0, 2, 4],
137 );
138 crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
139 },
140 }]
141}
142
143#[cfg(test)]
144#[path = "../unit_tests/rules/steinertree_ilp.rs"]
145mod tests;