LeetCode 3197 - Find the Minimum Area to Cover All Ones II
The problem gives us a binary matrix grid, where each cell contains either 0 or 1. Our goal is to place exactly three non-overlapping axis-aligned rectangles so that every cell containing 1 is covered by at least one rectangle.
Difficulty: 🔴 Hard
Topics: Array, Matrix, Enumeration
Solution
Problem Understanding
The problem gives us a binary matrix grid, where each cell contains either 0 or 1. Our goal is to place exactly three non-overlapping axis-aligned rectangles so that every cell containing 1 is covered by at least one rectangle.
Each rectangle must satisfy several conditions:
- Its sides must be parallel to the grid axes.
- It must have non-zero area.
- The three rectangles cannot overlap, although touching edges is allowed.
- Every
1in the matrix must lie inside at least one of the rectangles.
The objective is to minimize the sum of the areas of the three rectangles.
A rectangle is defined by its top row, bottom row, left column, and right column. The area is:
$$(bottom - top + 1) \times (right - left + 1)$$
Importantly, rectangles may contain 0s inside them. We are not required to tightly wrap individual 1s unless doing so minimizes the total area.
The grid dimensions are at most 30 x 30, which is relatively small. However, brute forcing all possible rectangles and all ways to assign cells to rectangles would still explode combinatorially. The small constraints suggest that an optimized enumeration approach is expected.
A key observation is that any optimal arrangement of three non-overlapping rectangles partitions the grid into three disjoint regions using horizontal and vertical cuts. Since rectangles are axis-aligned and non-overlapping, the layout must fall into one of a small number of geometric partition patterns.
Several edge cases are important:
- The
1s may already form three isolated cells, in which case the answer is3. - Many
1s may cluster together, forcing one large rectangle. - A rectangle may legally contain large empty regions if that reduces the total number of rectangles needed.
- Rectangles may touch edges or corners, but they cannot overlap.
- The grid may contain only a few rows or columns, so careful boundary handling is required.
Approaches
Brute Force Approach
A naive solution would enumerate every possible rectangle in the grid and then try every combination of three rectangles.
For each triple of rectangles, we would verify:
- The rectangles do not overlap.
- Every
1in the grid is covered. - Compute the total area.
A 30 x 30 grid contains roughly:
$$O(m^2 n^2)$$
possible rectangles, because we choose top, bottom, left, and right boundaries.
That means the total number of rectangle triples becomes:
$$O((m^2 n^2)^3)$$
which is astronomically large and completely infeasible.
Even aggressive pruning would not make this practical.
Key Insight
The crucial observation is that three non-overlapping rectangles partition the grid in only a limited number of structural ways.
Instead of enumerating arbitrary rectangles, we enumerate partitions of the grid into three regions. For each region, we compute the minimum bounding rectangle containing all 1s inside that region.
Since the rectangles are independent after partitioning, the optimal rectangle for a region is simply the tight bounding box around its 1s.
There are only six meaningful partition patterns:
- Three horizontal strips
- Three vertical strips
- Top strip + bottom split vertically
- Bottom strip + top split vertically
- Left strip + right split horizontally
- Right strip + left split horizontally
For every partition, we compute:
- The minimal bounding rectangle area for each region
- The sum of the three areas
- The minimum over all partitions
Because the grid is small, exhaustive enumeration of partition lines is efficient.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((m²n²)³) | O(1) | Enumerates all rectangle triples |
| Optimal | O(m²n²(m+n)) | O(m²n²) | Enumerates only valid partition structures |
Algorithm Walkthrough
Step 1: Precompute Bounding Rectangle Areas
We define a helper function:
$$area(r1, c1, r2, c2)$$
This function returns the area of the smallest rectangle covering all 1s inside the subgrid.
To compute it:
-
Scan the subgrid
-
Track:
-
minimum row containing
1 -
maximum row containing
1 -
minimum column containing
1 -
maximum column containing
1
If the region contains no 1s, return 0.
Otherwise:
$$(maxRow - minRow + 1) \times (maxCol - minCol + 1)$$
Because many regions are queried repeatedly, we memoize results.
Step 2: Enumerate Three Horizontal Strips
We choose two horizontal cuts:
- First cut after row
i - Second cut after row
j
This creates:
- Rows
[0..i] - Rows
[i+1..j] - Rows
[j+1..m-1]
Each spans all columns.
We compute the minimal bounding rectangle area inside each strip and sum them.
Step 3: Enumerate Three Vertical Strips
Similarly, choose two vertical cuts:
- First cut after column
i - Second cut after column
j
This creates three vertical regions.
Again compute the sum of minimal bounding areas.
Step 4: Enumerate Mixed Partitions
Now consider layouts where one region occupies the full width or height, and the remaining space is split.
Example:
- Top strip
- Bottom-left region
- Bottom-right region
We enumerate:
- The separating horizontal cut
- The vertical split in the lower portion
We repeat this for all four asymmetric orientations.
Step 5: Track the Minimum
For every valid partition:
- Compute three region areas
- Sum them
- Update the global minimum
Why it works
Every set of three non-overlapping axis-aligned rectangles induces a planar partition of the grid. Any such arrangement can always be represented by one of the six partition structures enumerated above.
Within a fixed region, the minimum-area rectangle covering all 1s is uniquely the bounding box of those 1s. Therefore, evaluating all partition structures guarantees that we examine the optimal arrangement.
Since we take the minimum over all possible partitions, the algorithm always returns the globally optimal answer.
Python Solution
from typing import List
from functools import lru_cache
class Solution:
def minimumSum(self, grid: List[List[int]]) -> int:
rows = len(grid)
cols = len(grid[0])
@lru_cache(None)
def area(r1: int, c1: int, r2: int, c2: int) -> int:
min_row = rows
max_row = -1
min_col = cols
max_col = -1
for r in range(r1, r2 + 1):
for c in range(c1, c2 + 1):
if grid[r][c] == 1:
min_row = min(min_row, r)
max_row = max(max_row, r)
min_col = min(min_col, c)
max_col = max(max_col, c)
if max_row == -1:
return 0
return (max_row - min_row + 1) * (max_col - min_col + 1)
answer = float('inf')
# Three horizontal strips
for r1 in range(rows - 2):
for r2 in range(r1 + 1, rows - 1):
total = (
area(0, 0, r1, cols - 1) +
area(r1 + 1, 0, r2, cols - 1) +
area(r2 + 1, 0, rows - 1, cols - 1)
)
answer = min(answer, total)
# Three vertical strips
for c1 in range(cols - 2):
for c2 in range(c1 + 1, cols - 1):
total = (
area(0, 0, rows - 1, c1) +
area(0, c1 + 1, rows - 1, c2) +
area(0, c2 + 1, rows - 1, cols - 1)
)
answer = min(answer, total)
# Top strip + bottom split vertically
for r in range(rows - 1):
for c in range(cols - 1):
total = (
area(0, 0, r, cols - 1) +
area(r + 1, 0, rows - 1, c) +
area(r + 1, c + 1, rows - 1, cols - 1)
)
answer = min(answer, total)
# Bottom strip + top split vertically
for r in range(rows - 1):
for c in range(cols - 1):
total = (
area(0, 0, r, c) +
area(0, c + 1, r, cols - 1) +
area(r + 1, 0, rows - 1, cols - 1)
)
answer = min(answer, total)
# Left strip + right split horizontally
for c in range(cols - 1):
for r in range(rows - 1):
total = (
area(0, 0, rows - 1, c) +
area(0, c + 1, r, cols - 1) +
area(r + 1, c + 1, rows - 1, cols - 1)
)
answer = min(answer, total)
# Right strip + left split horizontally
for c in range(cols - 1):
for r in range(rows - 1):
total = (
area(0, 0, r, c) +
area(r + 1, 0, rows - 1, c) +
area(0, c + 1, rows - 1, cols - 1)
)
answer = min(answer, total)
return answer
The implementation begins by defining the memoized area() helper. This function computes the smallest bounding rectangle covering all 1s within a subgrid.
Memoization is essential because the same regions are queried repeatedly across different partition configurations.
The algorithm then systematically enumerates all six partition types. For each partition:
- The grid is divided into three disjoint regions.
- The minimum covering rectangle area for each region is computed.
- Their sum is compared against the current best answer.
The final answer is the minimum total area across all valid partitions.
Go Solution
package main
import "math"
func minimumSum(grid [][]int) int {
rows := len(grid)
cols := len(grid[0])
type Key struct {
r1, c1, r2, c2 int
}
cache := make(map[Key]int)
area := func(r1, c1, r2, c2 int) int {
key := Key{r1, c1, r2, c2}
if val, exists := cache[key]; exists {
return val
}
minRow := rows
maxRow := -1
minCol := cols
maxCol := -1
for r := r1; r <= r2; r++ {
for c := c1; c <= c2; c++ {
if grid[r][c] == 1 {
if r < minRow {
minRow = r
}
if r > maxRow {
maxRow = r
}
if c < minCol {
minCol = c
}
if c > maxCol {
maxCol = c
}
}
}
}
if maxRow == -1 {
cache[key] = 0
return 0
}
result := (maxRow - minRow + 1) * (maxCol - minCol + 1)
cache[key] = result
return result
}
answer := math.MaxInt32
// Three horizontal strips
for r1 := 0; r1 < rows-2; r1++ {
for r2 := r1 + 1; r2 < rows-1; r2++ {
total :=
area(0, 0, r1, cols-1) +
area(r1+1, 0, r2, cols-1) +
area(r2+1, 0, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
// Three vertical strips
for c1 := 0; c1 < cols-2; c1++ {
for c2 := c1 + 1; c2 < cols-1; c2++ {
total :=
area(0, 0, rows-1, c1) +
area(0, c1+1, rows-1, c2) +
area(0, c2+1, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
// Top strip + bottom split vertically
for r := 0; r < rows-1; r++ {
for c := 0; c < cols-1; c++ {
total :=
area(0, 0, r, cols-1) +
area(r+1, 0, rows-1, c) +
area(r+1, c+1, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
// Bottom strip + top split vertically
for r := 0; r < rows-1; r++ {
for c := 0; c < cols-1; c++ {
total :=
area(0, 0, r, c) +
area(0, c+1, r, cols-1) +
area(r+1, 0, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
// Left strip + right split horizontally
for c := 0; c < cols-1; c++ {
for r := 0; r < rows-1; r++ {
total :=
area(0, 0, rows-1, c) +
area(0, c+1, r, cols-1) +
area(r+1, c+1, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
// Right strip + left split horizontally
for c := 0; c < cols-1; c++ {
for r := 0; r < rows-1; r++ {
total :=
area(0, 0, r, c) +
area(r+1, 0, rows-1, c) +
area(0, c+1, rows-1, cols-1)
if total < answer {
answer = total
}
}
}
return answer
}
The Go implementation mirrors the Python logic closely. Since Go does not support decorators like lru_cache, we manually memoize rectangle areas using a map.
The Key struct uniquely identifies a subgrid by storing its boundaries.
Go integers are sufficient because the maximum possible area is only:
$$30 \times 30 = 900$$
so overflow is not a concern.
Worked Examples
Example 1
Input:
grid = [
[1,0,1],
[1,1,1]
]
The algorithm considers all partition structures.
One optimal partition is:
| Region | Covered Cells | Bounding Rectangle | Area |
|---|---|---|---|
| Left | (0,0), (1,0) | rows 0-1, cols 0-0 | 2 |
| Middle | (1,1) | rows 1-1, cols 1-1 | 1 |
| Right | (0,2), (1,2) | rows 0-1, cols 2-2 | 2 |
Total:
$$2 + 1 + 2 = 5$$
No other partition produces a smaller total.
Final answer:
5
Example 2
Input:
grid = [
[1,0,1,0],
[0,1,0,1]
]
One optimal configuration:
| Region | Covered Cells | Bounding Rectangle | Area |
|---|---|---|---|
| Top-left pair | (0,0), (0,2) | rows 0-0, cols 0-2 | 3 |
| Middle | (1,1) | single cell | 1 |
| Right | (1,3) | single cell | 1 |
Total:
$$3 + 1 + 1 = 5$$
Final answer:
5
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m²n²(m+n)) | Enumerating all partition cuts and computing cached subgrid areas |
| Space | O(m²n²) | Memoization of subgrid area computations |
The grid dimensions are at most 30 x 30, so this complexity is comfortably fast enough.
The expensive operation is computing bounding boxes for subgrids, but memoization ensures each subgrid is processed only once.
Test Cases
from typing import List
s = Solution()
# Provided example 1
assert s.minimumSum([[1,0,1],[1,1,1]]) == 5
# Provided example 2
assert s.minimumSum([[1,0,1,0],[0,1,0,1]]) == 5
# Three isolated ones
assert s.minimumSum([
[1,0,0],
[0,1,0],
[0,0,1]
]) == 3
# All ones in a single row
assert s.minimumSum([
[1,1,1]
]) == 3
# All ones in a single column
assert s.minimumSum([
[1],
[1],
[1]
]) == 3
# Dense block
assert s.minimumSum([
[1,1],
[1,1]
]) == 4
# Sparse corners
assert s.minimumSum([
[1,0,0,1],
[0,0,0,0],
[1,0,0,1]
]) == 6
# Large empty gaps
assert s.minimumSum([
[1,0,0,0,1],
[0,0,0,0,0],
[0,1,0,0,0]
]) == 5
# Multiple clustered groups
assert s.minimumSum([
[1,1,0,0],
[1,1,0,1],
[0,0,0,1]
]) == 6
Test Summary
| Test | Why |
|---|---|
| Example 1 | Validates mixed partition handling |
| Example 2 | Validates disconnected points |
| Three isolated ones | Ensures minimal area can be exactly 3 |
| Single row | Tests horizontal edge case |
| Single column | Tests vertical edge case |
| Dense block | Ensures rectangles may overlap logically via partitioning |
| Sparse corners | Tests distant groups |
| Large empty gaps | Verifies rectangles may include zeros |
| Multiple clustered groups | Tests irregular clustering |
Edge Cases
One important edge case occurs when all 1s are isolated from each other. In this situation, the optimal strategy is often to assign one rectangle per cell, producing a total area equal to the number of covered cells. A careless implementation might accidentally merge distant cells into unnecessarily large rectangles. Our solution avoids this because every partition computes the tight bounding box independently.
Another tricky case occurs when the grid has only one row or one column. Many partitioning algorithms accidentally assume at least two dimensions exist. Our implementation carefully uses loop bounds such as rows - 1 and cols - 1, ensuring partitions remain valid even in degenerate grids.
A third subtle case is when a region contains no 1s at all. If this is not handled carefully, the bounding rectangle computation may produce invalid coordinates or negative areas. Our area() helper explicitly returns 0 when no 1 exists inside a region, which safely handles empty partitions.
A final edge case involves rectangles touching at borders. The problem allows touching but forbids overlapping. Since our partitions divide the grid into disjoint regions using exact cut lines, rectangles may share edges naturally without ever overlapping, which exactly matches the problem requirements.