Skip to main content

problemreductions/models/algebraic/
minimum_weight_solution_to_linear_equations.rs

1//! Minimum Weight Solution to Linear Equations problem implementation.
2//!
3//! Given an n×m integer matrix A and integer vector b, find a rational vector y
4//! with Ay = b that minimizes the number of non-zero entries (Hamming weight).
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Min;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "MinimumWeightSolutionToLinearEquations",
14        display_name: "Minimum Weight Solution to Linear Equations",
15        aliases: &[],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Find a rational solution to Ay=b minimizing the number of non-zero entries",
19        fields: &[
20            FieldInfo { name: "matrix", type_name: "Vec<Vec<i64>>", description: "n×m integer matrix A" },
21            FieldInfo { name: "rhs", type_name: "Vec<i64>", description: "right-hand side vector b of length n" },
22        ],
23    }
24}
25
26/// Minimum Weight Solution to Linear Equations.
27///
28/// Given an n×m integer matrix A and an integer vector b, find a rational
29/// vector y with Ay = b that minimizes ||y||_0 (the number of non-zero
30/// entries, i.e., the Hamming weight of y).
31///
32/// # Representation
33///
34/// Each of the m columns is a binary variable: `x_j = 1` means column j is
35/// selected (i.e., y_j may be non-zero). The evaluator checks whether the
36/// restricted system (using only selected columns) is consistent over the
37/// rationals, and returns the count of selected columns if so.
38///
39/// # Example
40///
41/// ```
42/// use problemreductions::models::algebraic::MinimumWeightSolutionToLinearEquations;
43/// use problemreductions::{Problem, Solver, BruteForce};
44///
45/// let matrix = vec![
46///     vec![1, 2, 3, 1],
47///     vec![2, 1, 1, 3],
48/// ];
49/// let rhs = vec![5, 4];
50/// let problem = MinimumWeightSolutionToLinearEquations::new(matrix, rhs);
51/// let solver = BruteForce::new();
52/// let witness = solver.find_witness(&problem);
53/// assert!(witness.is_some());
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MinimumWeightSolutionToLinearEquations {
57    /// The n×m integer matrix A.
58    matrix: Vec<Vec<i64>>,
59    /// The right-hand side vector b of length n.
60    rhs: Vec<i64>,
61}
62
63impl MinimumWeightSolutionToLinearEquations {
64    /// Create a new MinimumWeightSolutionToLinearEquations instance.
65    ///
66    /// # Panics
67    ///
68    /// Panics if the matrix is empty, rows have inconsistent lengths,
69    /// rhs length does not match the number of rows, or there are no columns.
70    pub fn new(matrix: Vec<Vec<i64>>, rhs: Vec<i64>) -> Self {
71        assert!(!matrix.is_empty(), "Matrix must have at least one row");
72        let num_cols = matrix[0].len();
73        assert!(num_cols > 0, "Matrix must have at least one column");
74        for row in &matrix {
75            assert_eq!(row.len(), num_cols, "All rows must have the same length");
76        }
77        assert_eq!(
78            rhs.len(),
79            matrix.len(),
80            "RHS length must equal number of rows"
81        );
82        Self { matrix, rhs }
83    }
84
85    /// Returns a reference to the matrix A.
86    pub fn matrix(&self) -> &[Vec<i64>] {
87        &self.matrix
88    }
89
90    /// Returns a reference to the right-hand side vector b.
91    pub fn rhs(&self) -> &[i64] {
92        &self.rhs
93    }
94
95    /// Returns the number of equations (rows of A).
96    pub fn num_equations(&self) -> usize {
97        self.matrix.len()
98    }
99
100    /// Returns the number of variables (columns of A).
101    pub fn num_variables(&self) -> usize {
102        self.matrix[0].len()
103    }
104
105    /// Check whether the system restricted to the given column indices is
106    /// consistent over the rationals. Uses integer Gaussian elimination on
107    /// the augmented matrix [A'|b] with i128 arithmetic.
108    fn is_consistent(&self, columns: &[usize]) -> bool {
109        let n = self.num_equations();
110        let k = columns.len();
111
112        // Build augmented matrix [A'|b] as i128 to avoid overflow.
113        // Each row has k coefficient columns + 1 rhs column.
114        let mut aug: Vec<Vec<i128>> = (0..n)
115            .map(|i| {
116                let mut row = Vec::with_capacity(k + 1);
117                for &j in columns {
118                    row.push(self.matrix[i][j] as i128);
119                }
120                row.push(self.rhs[i] as i128);
121                row
122            })
123            .collect();
124
125        let mut pivot_row = 0;
126        for col in 0..k {
127            // Find a non-zero entry in column `col` at or below `pivot_row`.
128            let Some(swap_row) = (pivot_row..n).find(|&r| aug[r][col] != 0) else {
129                continue;
130            };
131            aug.swap(pivot_row, swap_row);
132
133            let pivot_val = aug[pivot_row][col];
134            let pivot_row_snapshot = aug[pivot_row].clone();
135            // Eliminate all other rows.
136            for (r, row) in aug.iter_mut().enumerate() {
137                if r == pivot_row {
138                    continue;
139                }
140                let factor = row[col];
141                if factor == 0 {
142                    continue;
143                }
144                // row[r] = pivot_val * row[r] - factor * row[pivot_row]
145                for (cell, &pv) in row.iter_mut().zip(pivot_row_snapshot.iter()) {
146                    *cell = pivot_val * *cell - factor * pv;
147                }
148            }
149            pivot_row += 1;
150        }
151
152        // Check for inconsistency: any row with all-zero coefficients but
153        // non-zero rhs means the system is inconsistent.
154        for row in &aug[pivot_row..n] {
155            if row[k] != 0 {
156                return false;
157            }
158        }
159        true
160    }
161}
162
163impl Problem for MinimumWeightSolutionToLinearEquations {
164    const NAME: &'static str = "MinimumWeightSolutionToLinearEquations";
165    type Value = Min<usize>;
166
167    fn variant() -> Vec<(&'static str, &'static str)> {
168        crate::variant_params![]
169    }
170
171    fn dims(&self) -> Vec<usize> {
172        vec![2; self.num_variables()]
173    }
174
175    fn evaluate(&self, config: &[usize]) -> Min<usize> {
176        if config.len() != self.num_variables() {
177            return Min(None);
178        }
179        if config.iter().any(|&v| v >= 2) {
180            return Min(None);
181        }
182
183        let columns: Vec<usize> = config
184            .iter()
185            .enumerate()
186            .filter(|(_, &v)| v == 1)
187            .map(|(j, _)| j)
188            .collect();
189
190        if columns.is_empty() {
191            // No columns selected — consistent iff b = 0.
192            if self.rhs.iter().all(|&v| v == 0) {
193                return Min(Some(0));
194            } else {
195                return Min(None);
196            }
197        }
198
199        if self.is_consistent(&columns) {
200            Min(Some(columns.len()))
201        } else {
202            Min(None)
203        }
204    }
205}
206
207crate::declare_variants! {
208    default MinimumWeightSolutionToLinearEquations => "2^num_variables",
209}
210
211#[cfg(feature = "example-db")]
212pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
213    // A = [[1,2,3,1],[2,1,1,3]], b = [5,4], m=4, n=2
214    // Config [1,1,0,0]: select columns 0,1. Submatrix [[1,2],[2,1]].
215    // Solve [1,2;2,1]y=[5,4] → y=(1,2). Consistent. Min(2).
216    let matrix = vec![vec![1, 2, 3, 1], vec![2, 1, 1, 3]];
217    let rhs = vec![5, 4];
218    vec![crate::example_db::specs::ModelExampleSpec {
219        id: "minimum_weight_solution_to_linear_equations",
220        instance: Box::new(MinimumWeightSolutionToLinearEquations::new(matrix, rhs)),
221        optimal_config: vec![1, 1, 0, 0],
222        optimal_value: serde_json::json!(2),
223    }]
224}
225
226#[cfg(test)]
227#[path = "../../unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs"]
228mod tests;