|
| 1 | +//! Reduction from SteinerTree to ILP (Integer Linear Programming). |
| 2 | +//! |
| 3 | +//! Uses the standard rooted multi-commodity flow formulation: |
| 4 | +//! - Variables: edge selectors `y_e` plus directed flow variables `f^t_(u,v)` |
| 5 | +//! for each non-root terminal `t` |
| 6 | +//! - Constraints: flow conservation for each commodity and capacity linking |
| 7 | +//! `f^t_(u,v) <= y_e` |
| 8 | +//! - Objective: minimize the total weight of selected edges |
| 9 | +
|
| 10 | +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; |
| 11 | +use crate::models::graph::SteinerTree; |
| 12 | +use crate::reduction; |
| 13 | +use crate::rules::traits::{ReduceTo, ReductionResult}; |
| 14 | +use crate::topology::{Graph, SimpleGraph}; |
| 15 | + |
| 16 | +/// Result of reducing SteinerTree to ILP. |
| 17 | +/// |
| 18 | +/// Variable layout (all binary): |
| 19 | +/// - `y_e` for each undirected source edge `e` (indices `0..m`) |
| 20 | +/// - `f^t_(u,v)` and `f^t_(v,u)` for each non-root terminal `t` and each source edge |
| 21 | +/// `(u, v)` (indices `m..m + 2m(k-1)`) |
| 22 | +#[derive(Debug, Clone)] |
| 23 | +pub struct ReductionSteinerTreeToILP { |
| 24 | + target: ILP<bool>, |
| 25 | + num_edges: usize, |
| 26 | +} |
| 27 | + |
| 28 | +impl 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 | +)] |
| 47 | +impl 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")] |
| 126 | +pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> { |
| 127 | + use crate::export::SolutionPair; |
| 128 | + |
| 129 | + vec![crate::example_db::specs::RuleExampleSpec { |
| 130 | + id: "steinertree_to_ilp", |
| 131 | + build: || { |
| 132 | + let source = SteinerTree::new( |
| 133 | + SimpleGraph::new( |
| 134 | + 5, |
| 135 | + vec![(0, 1), (1, 2), (1, 3), (3, 4), (0, 3), (3, 2), (2, 4)], |
| 136 | + ), |
| 137 | + vec![2, 2, 1, 1, 5, 5, 6], |
| 138 | + vec![0, 2, 4], |
| 139 | + ); |
| 140 | + crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>( |
| 141 | + source, |
| 142 | + SolutionPair { |
| 143 | + source_config: vec![1, 1, 1, 1, 0, 0, 0], |
| 144 | + target_config: vec![ |
| 145 | + 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, |
| 146 | + 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, |
| 147 | + ], |
| 148 | + }, |
| 149 | + ) |
| 150 | + }, |
| 151 | + }] |
| 152 | +} |
| 153 | + |
| 154 | +#[cfg(test)] |
| 155 | +#[path = "../unit_tests/rules/steinertree_ilp.rs"] |
| 156 | +mod tests; |
0 commit comments