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