Skip to main content

problemreductions/models/algebraic/
consecutive_block_minimization.rs

1//! Consecutive Block Minimization (CBM) problem implementation.
2//!
3//! Given an m x n binary matrix A and a positive integer K,
4//! determine whether there exists a permutation of the columns of A
5//! such that the resulting matrix has at most K maximal blocks of
6//! consecutive 1-entries (summed over all rows).
7//!
8//! A "block" is a maximal contiguous run of 1-entries in a row.
9//! This is problem SR17 in Garey & Johnson.
10
11use crate::registry::{FieldInfo, ProblemSchemaEntry};
12use crate::traits::Problem;
13use serde::{Deserialize, Serialize};
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "ConsecutiveBlockMinimization",
18        display_name: "Consecutive Block Minimization",
19        aliases: &["CBM"],
20        dimensions: &[],
21        module_path: module_path!(),
22        description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s",
23        fields: &[
24            FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "Binary matrix A (m x n)" },
25            FieldInfo { name: "bound", type_name: "i64", description: "Upper bound K on total consecutive blocks" },
26        ],
27    }
28}
29
30/// Consecutive Block Minimization (CBM) problem.
31///
32/// Given an m x n binary matrix A and a positive integer K,
33/// determine whether there exists a permutation of the columns of A
34/// such that the resulting matrix has at most K maximal blocks of
35/// consecutive 1-entries (summed over all rows).
36///
37/// # Example
38///
39/// ```
40/// use problemreductions::models::algebraic::ConsecutiveBlockMinimization;
41/// use problemreductions::{Problem, Solver, BruteForce};
42///
43/// // 2x3 binary matrix
44/// let problem = ConsecutiveBlockMinimization::new(
45///     vec![
46///         vec![true, false, true],
47///         vec![false, true, true],
48///     ],
49///     2,
50/// );
51///
52/// let solver = BruteForce::new();
53/// let solutions = solver.find_all_witnesses(&problem);
54///
55/// // Verify solutions satisfy the block bound
56/// for sol in solutions {
57///     assert!(problem.evaluate(&sol));
58/// }
59/// ```
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(
62    try_from = "ConsecutiveBlockMinimizationDef",
63    into = "ConsecutiveBlockMinimizationDef"
64)]
65pub struct ConsecutiveBlockMinimization {
66    /// The binary matrix A (m x n).
67    matrix: Vec<Vec<bool>>,
68    /// Number of rows (m).
69    num_rows: usize,
70    /// Number of columns (n).
71    num_cols: usize,
72    /// Upper bound K on total consecutive blocks.
73    bound: i64,
74}
75
76impl ConsecutiveBlockMinimization {
77    /// Create a new ConsecutiveBlockMinimization problem.
78    ///
79    /// # Arguments
80    /// * `matrix` - The m x n binary matrix
81    /// * `bound` - Upper bound on total consecutive blocks
82    ///
83    /// # Panics
84    /// Panics if rows have inconsistent lengths.
85    pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
86        Self::try_new(matrix, bound).unwrap_or_else(|err| panic!("{err}"))
87    }
88
89    /// Create a new ConsecutiveBlockMinimization problem, returning an error
90    /// instead of panicking when the matrix is ragged.
91    pub fn try_new(matrix: Vec<Vec<bool>>, bound: i64) -> Result<Self, String> {
92        let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?;
93        Ok(Self {
94            matrix,
95            num_rows,
96            num_cols,
97            bound,
98        })
99    }
100
101    /// Get the binary matrix.
102    pub fn matrix(&self) -> &[Vec<bool>] {
103        &self.matrix
104    }
105
106    /// Get the number of rows.
107    pub fn num_rows(&self) -> usize {
108        self.num_rows
109    }
110
111    /// Get the number of columns.
112    pub fn num_cols(&self) -> usize {
113        self.num_cols
114    }
115
116    /// Get the upper bound K.
117    pub fn bound(&self) -> i64 {
118        self.bound
119    }
120
121    /// Count the total number of maximal consecutive blocks of 1s
122    /// when columns are permuted according to `config`.
123    ///
124    /// `config[position] = column_index` defines the column permutation.
125    /// Returns `Some(total_blocks)` if the config is a valid permutation,
126    /// or `None` if it is not (wrong length, duplicate columns, or out-of-range).
127    pub fn count_consecutive_blocks(&self, config: &[usize]) -> Option<usize> {
128        if config.len() != self.num_cols {
129            return None;
130        }
131
132        // Validate permutation: all values distinct and in 0..num_cols.
133        let mut seen = vec![false; self.num_cols];
134        for &col in config {
135            if col >= self.num_cols || seen[col] {
136                return None;
137            }
138            seen[col] = true;
139        }
140
141        let mut total_blocks = 0;
142        for row in &self.matrix {
143            let mut in_block = false;
144            for &pos in config {
145                if row[pos] {
146                    if !in_block {
147                        total_blocks += 1;
148                        in_block = true;
149                    }
150                } else {
151                    in_block = false;
152                }
153            }
154        }
155
156        Some(total_blocks)
157    }
158}
159
160impl Problem for ConsecutiveBlockMinimization {
161    const NAME: &'static str = "ConsecutiveBlockMinimization";
162    type Value = crate::types::Or;
163
164    fn dims(&self) -> Vec<usize> {
165        vec![self.num_cols; self.num_cols]
166    }
167
168    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
169        crate::types::Or({
170            match self.count_consecutive_blocks(config) {
171                Some(total) => (total as i64) <= self.bound,
172                None => false,
173            }
174        })
175    }
176
177    fn variant() -> Vec<(&'static str, &'static str)> {
178        crate::variant_params![]
179    }
180
181    fn num_variables(&self) -> usize {
182        self.num_cols
183    }
184}
185
186crate::declare_variants! {
187    default ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols",
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191struct ConsecutiveBlockMinimizationDef {
192    matrix: Vec<Vec<bool>>,
193    bound: i64,
194}
195
196impl TryFrom<ConsecutiveBlockMinimizationDef> for ConsecutiveBlockMinimization {
197    type Error = String;
198
199    fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result<Self, Self::Error> {
200        Self::try_new(value.matrix, value.bound)
201    }
202}
203
204impl From<ConsecutiveBlockMinimization> for ConsecutiveBlockMinimizationDef {
205    fn from(value: ConsecutiveBlockMinimization) -> Self {
206        Self {
207            matrix: value.matrix,
208            bound: value.bound,
209        }
210    }
211}
212
213fn validate_matrix_dimensions(matrix: &[Vec<bool>]) -> Result<(usize, usize), String> {
214    let num_rows = matrix.len();
215    let num_cols = matrix.first().map_or(0, Vec::len);
216
217    if matrix.iter().any(|row| row.len() != num_cols) {
218        return Err("all matrix rows must have the same length".to_string());
219    }
220
221    Ok((num_rows, num_cols))
222}
223
224#[cfg(feature = "example-db")]
225pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
226    // Adjacency matrix of path graph P_6, K=6 (one block per row).
227    // Issue #420 Instance 2.
228    vec![crate::example_db::specs::ModelExampleSpec {
229        id: "consecutive_block_minimization",
230        instance: Box::new(ConsecutiveBlockMinimization::new(
231            vec![
232                vec![false, true, false, false, false, false],
233                vec![true, false, true, false, false, false],
234                vec![false, true, false, true, false, false],
235                vec![false, false, true, false, true, false],
236                vec![false, false, false, true, false, true],
237                vec![false, false, false, false, true, false],
238            ],
239            6,
240        )),
241        optimal_config: vec![0, 2, 4, 1, 3, 5],
242        optimal_value: serde_json::json!(true),
243    }]
244}
245
246#[cfg(test)]
247#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"]
248mod tests;