Skip to main content

problemreductions/models/misc/
rectilinear_picture_compression.rs

1//! Rectilinear Picture Compression problem implementation.
2//!
3//! Given an m x n binary matrix M and a nonnegative integer K, determine whether
4//! there exists a collection of at most K axis-aligned all-1 rectangles that
5//! covers precisely the 1-entries of M. Each rectangle (r1, c1, r2, c2) with
6//! r1 <= r2, c1 <= c2 covers entries M[i][j] for r1 <= i <= r2, c1 <= j <= c2,
7//! and every covered entry must be 1.
8
9use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::traits::Problem;
11use serde::de::Deserializer;
12use serde::{Deserialize, Serialize};
13
14type Rectangle = (usize, usize, usize, usize);
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "RectilinearPictureCompression",
19        display_name: "Rectilinear Picture Compression",
20        aliases: &[],
21        dimensions: &[],
22        category: crate::registry::ProblemCategory::Misc,
23        module_path: module_path!(),
24        description: "Cover all 1-entries of a binary matrix with at most K axis-aligned all-1 rectangles",
25        fields: &[
26            FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "m x n binary matrix" },
27            FieldInfo { name: "bound", type_name: "i64", description: "Maximum number of rectangles allowed" },
28        ],
29    }
30}
31
32/// The Rectilinear Picture Compression problem.
33///
34/// Given an m x n binary matrix M and a nonnegative integer K, determine whether
35/// there exists a collection of at most K axis-aligned all-1 rectangles that
36/// covers precisely the 1-entries of M.
37///
38/// # Representation
39///
40/// The configuration space consists of the maximal all-1 rectangles in the
41/// matrix. Each variable is binary: 1 if the rectangle is selected, 0 otherwise.
42/// The problem is satisfiable iff the selected rectangles number at most K and
43/// their union covers all 1-entries.
44///
45/// # Example
46///
47/// ```
48/// use problemreductions::models::misc::RectilinearPictureCompression;
49/// use problemreductions::{Problem, BruteForce};
50///
51/// let matrix = vec![
52///     vec![true, true, false, false],
53///     vec![true, true, false, false],
54///     vec![false, false, true, true],
55///     vec![false, false, true, true],
56/// ];
57/// let problem = RectilinearPictureCompression::new(matrix, 2);
58/// let solver = BruteForce::new();
59/// let solution = solver.solve(&problem).unwrap();
60/// assert!(solution.is_some());
61/// ```
62#[derive(Debug, Clone, Serialize)]
63pub struct RectilinearPictureCompression {
64    matrix: Vec<Vec<bool>>,
65    bound: i64,
66    #[serde(skip)]
67    maximal_rects: Vec<Rectangle>,
68}
69
70impl<'de> Deserialize<'de> for RectilinearPictureCompression {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        #[derive(Deserialize)]
76        struct Inner {
77            matrix: Vec<Vec<bool>>,
78            bound: i64,
79        }
80        let inner = Inner::deserialize(deserializer)?;
81        Ok(Self::new(inner.matrix, inner.bound))
82    }
83}
84
85impl RectilinearPictureCompression {
86    /// Create a new RectilinearPictureCompression instance.
87    ///
88    /// # Panics
89    ///
90    /// Panics if `matrix` is empty or has inconsistent row lengths.
91    pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
92        assert!(!matrix.is_empty(), "Matrix must not be empty");
93        let cols = matrix[0].len();
94        assert!(cols > 0, "Matrix must have at least one column");
95        assert!(
96            matrix.iter().all(|row| row.len() == cols),
97            "All rows must have the same length"
98        );
99        let mut instance = Self {
100            matrix,
101            bound,
102            maximal_rects: Vec::new(),
103        };
104        instance.maximal_rects = instance.compute_maximal_rectangles();
105        instance
106    }
107
108    /// Returns the number of rows in the matrix.
109    pub fn num_rows(&self) -> usize {
110        self.matrix.len()
111    }
112
113    /// Returns the number of columns in the matrix.
114    pub fn num_cols(&self) -> usize {
115        self.matrix[0].len()
116    }
117
118    /// Returns the bound K.
119    pub fn bound(&self) -> i64 {
120        self.bound
121    }
122
123    /// Returns a reference to the binary matrix.
124    pub fn matrix(&self) -> &[Vec<bool>] {
125        &self.matrix
126    }
127
128    /// Returns the precomputed maximal all-1 sub-rectangles.
129    ///
130    /// Each rectangle is `(r1, c1, r2, c2)` covering rows `r1..=r2` and
131    /// columns `c1..=c2`.
132    pub fn maximal_rectangles(&self) -> &[Rectangle] {
133        &self.maximal_rects
134    }
135
136    fn build_prefix_sum(&self) -> Vec<Vec<usize>> {
137        let m = self.num_rows();
138        let n = self.num_cols();
139        let mut prefix_sum = vec![vec![0; n + 1]; m + 1];
140
141        for r in 0..m {
142            let mut row_sum = 0;
143            for c in 0..n {
144                row_sum += usize::from(self.matrix[r][c]);
145                prefix_sum[r + 1][c + 1] = prefix_sum[r][c + 1] + row_sum;
146            }
147        }
148
149        prefix_sum
150    }
151
152    fn range_is_all_ones(
153        prefix_sum: &[Vec<usize>],
154        r1: usize,
155        c1: usize,
156        r2: usize,
157        c2: usize,
158    ) -> bool {
159        let area = (r2 - r1 + 1) * (c2 - c1 + 1);
160        let sum = prefix_sum[r2 + 1][c2 + 1] + prefix_sum[r1][c1]
161            - prefix_sum[r1][c2 + 1]
162            - prefix_sum[r2 + 1][c1];
163        sum == area
164    }
165
166    /// Enumerate all maximal all-1 sub-rectangles in the matrix.
167    ///
168    /// A rectangle is maximal if it cannot be extended one step left, right,
169    /// up, or down while remaining all-1. The result is sorted lexicographically.
170    fn compute_maximal_rectangles(&self) -> Vec<Rectangle> {
171        let m = self.num_rows();
172        let n = self.num_cols();
173
174        // Step 1: Enumerate right-maximal candidate rectangles by fixing
175        // (r1, c1, r2) and taking the widest all-1 prefix common to rows r1..=r2.
176        let mut candidates = Vec::new();
177        for r1 in 0..m {
178            for c1 in 0..n {
179                if !self.matrix[r1][c1] {
180                    continue;
181                }
182                // Find the rightmost column from c1 that is all-1 in row r1.
183                let mut c_max = n;
184                for c in c1..n {
185                    if !self.matrix[r1][c] {
186                        c_max = c;
187                        break;
188                    }
189                }
190                // Extend downward row by row, narrowing column range.
191                let mut c_end = c_max; // exclusive upper bound on columns
192                for r2 in r1..m {
193                    // Narrow c_end based on row r2.
194                    let mut new_c_end = c1;
195                    for c in c1..c_end {
196                        if self.matrix[r2][c] {
197                            new_c_end = c + 1;
198                        } else {
199                            break;
200                        }
201                    }
202                    if new_c_end <= c1 {
203                        break;
204                    }
205                    c_end = new_c_end;
206                    candidates.push((r1, c1, r2, c_end - 1));
207                }
208            }
209        }
210
211        // Step 2: Remove duplicates.
212        candidates.sort();
213        candidates.dedup();
214
215        // Step 3: Filter to keep only rectangles that cannot be extended in
216        // any cardinal direction. A 2D prefix sum makes each extension check O(1).
217        let prefix_sum = self.build_prefix_sum();
218        candidates
219            .into_iter()
220            .filter(|&(r1, c1, r2, c2)| {
221                let can_extend_left =
222                    c1 > 0 && Self::range_is_all_ones(&prefix_sum, r1, c1 - 1, r2, c1 - 1);
223                let can_extend_right =
224                    c2 + 1 < n && Self::range_is_all_ones(&prefix_sum, r1, c2 + 1, r2, c2 + 1);
225                let can_extend_up =
226                    r1 > 0 && Self::range_is_all_ones(&prefix_sum, r1 - 1, c1, r1 - 1, c2);
227                let can_extend_down =
228                    r2 + 1 < m && Self::range_is_all_ones(&prefix_sum, r2 + 1, c1, r2 + 1, c2);
229
230                !(can_extend_left || can_extend_right || can_extend_up || can_extend_down)
231            })
232            .collect()
233    }
234}
235
236impl Problem for RectilinearPictureCompression {
237    const NAME: &'static str = "RectilinearPictureCompression";
238    type Solution = Vec<bool>;
239    type Value = crate::types::Or;
240
241    crate::problem_parameters![("num_cols", num_cols), ("num_rows", num_rows),];
242
243    fn variant() -> Vec<(&'static str, &'static str)> {
244        crate::variant_params![]
245    }
246
247    fn evaluate(
248        &self,
249        config: &Self::Solution,
250    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
251        Ok({
252            crate::types::Or({
253                let rects = &self.maximal_rects;
254                if config.len() != rects.len() {
255                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
256                        "rectangle-selection length does not match the maximal rectangles".into(),
257                    ));
258                }
259                // Count selected rectangles.
260                let selected_count = config.iter().filter(|&&selected| selected).count();
261                let selected_count = i64::try_from(selected_count).map_err(|_| {
262                    crate::traits::EvaluationError::IntegerOverflow(
263                        "converting selected-rectangle count to i64".into(),
264                    )
265                })?;
266                if selected_count > self.bound {
267                    return Ok(crate::types::Or(false));
268                }
269
270                // Check that all 1-entries are covered.
271                let m = self.num_rows();
272                let n = self.num_cols();
273                let mut covered = vec![vec![false; n]; m];
274                for (i, &x) in config.iter().enumerate() {
275                    if x {
276                        let (r1, c1, r2, c2) = rects[i];
277                        for row in &mut covered[r1..=r2] {
278                            for cell in &mut row[c1..=c2] {
279                                *cell = true;
280                            }
281                        }
282                    }
283                }
284
285                for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) {
286                    for (&entry, &cov) in row_m.iter().zip(row_c.iter()) {
287                        if entry && !cov {
288                            return Ok(crate::types::Or(false));
289                        }
290                    }
291                }
292
293                true
294            })
295        })
296    }
297}
298
299impl crate::solvers::BruteForceProblem for RectilinearPictureCompression {
300    fn dimensions(&self) -> Vec<usize> {
301        vec![2; self.maximal_rects.len()]
302    }
303}
304
305crate::declare_variants! {
306    default RectilinearPictureCompression => "2^(num_rows * num_cols)",
307}
308
309crate::register_brute_force! {
310    RectilinearPictureCompression decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
311}
312
313#[cfg(feature = "example-db")]
314pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
315    vec![crate::example_db::specs::ModelExampleSpec {
316        id: "rectilinear_picture_compression",
317        // Config: select both maximal rectangles (the two 2x2 blocks).
318        // The maximal rectangles for this matrix are exactly:
319        // (0,0,1,1) and (2,2,3,3), so config [1,1] selects both.
320        instance: Box::new(RectilinearPictureCompression::new(
321            vec![
322                vec![true, true, false, false],
323                vec![true, true, false, false],
324                vec![false, false, true, true],
325                vec![false, false, true, true],
326            ],
327            2,
328        )),
329        optimal_config: serde_json::json!(vec![true, true]),
330        optimal_value: serde_json::json!(true),
331    }]
332}
333
334#[cfg(test)]
335#[path = "../../unit_tests/models/misc/rectilinear_picture_compression.rs"]
336mod tests;