|
| 1 | +//! Reduction from ClosestString to ILP (Integer Linear Programming). |
| 2 | +//! |
| 3 | +//! Given an alphabet of size `q`, `n` input strings of common length `m`, and |
| 4 | +//! the goal of finding a center `c in Sigma^m` that minimizes the maximum |
| 5 | +//! Hamming distance to every input string, the natural encoding picks one |
| 6 | +//! alphabet symbol at every center position and constrains a radius variable |
| 7 | +//! to upper-bound every per-string Hamming distance: |
| 8 | +//! |
| 9 | +//! - Binary `x_{j, a} in {0, 1}` for `j in {0, ..., m - 1}` and `a in |
| 10 | +//! {0, ..., q - 1}`: `x_{j, a} = 1` iff the chosen center has symbol `a` at |
| 11 | +//! position `j`. |
| 12 | +//! - Nonnegative integer radius variable `R`. |
| 13 | +//! - Assignment constraint: `sum_a x_{j, a} = 1` for every position `j`. |
| 14 | +//! Because every ILP variable is a nonnegative integer, this also forces |
| 15 | +//! each `x_{j, a} in {0, 1}`. |
| 16 | +//! - Radius constraint per input string `s_i`: |
| 17 | +//! `R + sum_j x_{j, s_i[j]} >= m`, which is equivalent to `R >= d_H(c, s_i)`. |
| 18 | +//! - Objective: minimize `R`. |
| 19 | +//! |
| 20 | +//! Reference: Ming Li, Bin Ma, and Lusheng Wang, "On the closest string and |
| 21 | +//! substring problems," Journal of the ACM 49(2):157-171, 2002. |
| 22 | +//! <https://doi.org/10.1145/506147.506150> |
| 23 | +
|
| 24 | +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; |
| 25 | +use crate::models::misc::ClosestString; |
| 26 | +use crate::reduction; |
| 27 | +use crate::rules::traits::{ReduceTo, ReductionResult}; |
| 28 | + |
| 29 | +/// Result of reducing ClosestString to ILP. |
| 30 | +/// |
| 31 | +/// Variable layout (`ILP<i32>`, all non-negative): |
| 32 | +/// - `x_{j, a}` at index `j * alphabet_size + a` for `j in [0, m)` and |
| 33 | +/// `a in [0, q)`, bounded to `{0, 1}`. |
| 34 | +/// - `R` (radius) at index `m * q`, an integer in `[0, m]`. |
| 35 | +#[derive(Debug, Clone)] |
| 36 | +pub struct ReductionClosestStringToILP { |
| 37 | + target: ILP<i32>, |
| 38 | + alphabet_size: usize, |
| 39 | + string_length: usize, |
| 40 | +} |
| 41 | + |
| 42 | +impl ReductionResult for ReductionClosestStringToILP { |
| 43 | + type Source = ClosestString; |
| 44 | + type Target = ILP<i32>; |
| 45 | + |
| 46 | + fn target_problem(&self) -> &ILP<i32> { |
| 47 | + &self.target |
| 48 | + } |
| 49 | + |
| 50 | + /// Decode the integer ILP assignment into the source center config. |
| 51 | + /// |
| 52 | + /// For every position `j`, choose the unique alphabet symbol `a` with |
| 53 | + /// `x_{j, a} = 1`. If the target assignment is missing or none of the |
| 54 | + /// per-position `x_{j, *}` variables are set to 1, we fall back to symbol |
| 55 | + /// `0` so the returned vector still has the expected length; partial / |
| 56 | + /// infeasible ILP solutions are the caller's responsibility. |
| 57 | + fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> { |
| 58 | + let q = self.alphabet_size; |
| 59 | + (0..self.string_length) |
| 60 | + .map(|j| { |
| 61 | + (0..q) |
| 62 | + .find(|&a| target_solution.get(j * q + a).copied().unwrap_or(0) == 1) |
| 63 | + .unwrap_or(0) |
| 64 | + }) |
| 65 | + .collect() |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +#[reduction( |
| 70 | + overhead = { |
| 71 | + num_vars = "alphabet_size * string_length + 1", |
| 72 | + num_constraints = "string_length + num_strings", |
| 73 | + } |
| 74 | +)] |
| 75 | +impl ReduceTo<ILP<i32>> for ClosestString { |
| 76 | + type Result = ReductionClosestStringToILP; |
| 77 | + |
| 78 | + fn reduce_to(&self) -> Self::Result { |
| 79 | + let q = self.alphabet_size(); |
| 80 | + let m = self.string_length(); |
| 81 | + let strings = self.strings(); |
| 82 | + let n = strings.len(); |
| 83 | + |
| 84 | + let x_idx = |j: usize, a: usize| -> usize { j * q + a }; |
| 85 | + let r_idx = q * m; |
| 86 | + let num_vars = q * m + 1; |
| 87 | + |
| 88 | + let mut constraints: Vec<LinearConstraint> = Vec::with_capacity(m + n); |
| 89 | + |
| 90 | + // Assignment constraints: exactly one symbol per center position. |
| 91 | + // Together with the non-negativity built into ILP<i32>, this also |
| 92 | + // forces every x_{j, a} to lie in {0, 1}. |
| 93 | + for j in 0..m { |
| 94 | + let terms: Vec<(usize, f64)> = (0..q).map(|a| (x_idx(j, a), 1.0)).collect(); |
| 95 | + constraints.push(LinearConstraint::eq(terms, 1.0)); |
| 96 | + } |
| 97 | + |
| 98 | + // Radius constraints: R + sum_j x_{j, s_i[j]} >= m. |
| 99 | + // Equivalently, R >= m - sum_j x_{j, s_i[j]} = d_H(c, s_i). |
| 100 | + for s in strings.iter() { |
| 101 | + let mut terms: Vec<(usize, f64)> = Vec::with_capacity(m + 1); |
| 102 | + terms.push((r_idx, 1.0)); |
| 103 | + for (j, &symbol) in s.iter().enumerate() { |
| 104 | + terms.push((x_idx(j, symbol), 1.0)); |
| 105 | + } |
| 106 | + constraints.push(LinearConstraint::ge(terms, m as f64)); |
| 107 | + } |
| 108 | + |
| 109 | + // Objective: minimize R. |
| 110 | + let objective = vec![(r_idx, 1.0)]; |
| 111 | + |
| 112 | + let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize); |
| 113 | + |
| 114 | + ReductionClosestStringToILP { |
| 115 | + target, |
| 116 | + alphabet_size: q, |
| 117 | + string_length: m, |
| 118 | + } |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +#[cfg(feature = "example-db")] |
| 123 | +pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> { |
| 124 | + vec![crate::example_db::specs::RuleExampleSpec { |
| 125 | + id: "closeststring_to_ilp", |
| 126 | + build: || { |
| 127 | + // Canonical issue #1032 instance: binary alphabet, the four length-3 |
| 128 | + // strings 000, 011, 101, 110. Optimum radius is 2 (achieved by any |
| 129 | + // binary length-3 center, e.g. 000). |
| 130 | + let source = ClosestString::new( |
| 131 | + 2, |
| 132 | + vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]], |
| 133 | + ); |
| 134 | + crate::example_db::specs::rule_example_via_ilp::<_, i32>(source) |
| 135 | + }, |
| 136 | + }] |
| 137 | +} |
| 138 | + |
| 139 | +#[cfg(test)] |
| 140 | +#[path = "../unit_tests/rules/closeststring_ilp.rs"] |
| 141 | +mod tests; |
0 commit comments