1139. Largest 1-Bordered Square
Problem Description
The problem presents us with a 2-dimensional grid made up of 0
s and 1
s. The task is to determine the size of the largest square subgrid where all the cells on its border are filled with 1
s. The size of a subgrid is defined by the number of elements it contains. If no such subgrid exists, we are to return a size of 0
. This is a classic problem for exploring the concepts of dynamic programming in a 2D grid structure.
Intuition
The solution is built upon a dynamic programming approach, where we prepare auxiliary arrays down
and right
to store the maximum number of consecutive 1
s below each cell (including the cell itself) and to the right of each cell (including the cell itself), respectively.
The process is as follows:
- We iterate over the grid from bottom to top and from right to left. This reverse order helps us easily calculate the consecutive
1
s for thedown
andright
arrays as we can simply add1
to the count found in the next row or column. - After populating the
down
andright
arrays, we look for the largest square subgrid with all1
s on its border. We do this by trying to find a square of sizek
(starting from the largest possible size and decreasing the size) where in the top left corner of this square, both the number of consecutive1
s downward and rightward from this cell is at leastk
. - We also need to ensure that the bottom-left and top-right corners of the potential subgrid have at least
k
consecutive1
s upwards and leftwards to ensure the border is made entirely of1
s. - We do this by iterating over all possible positions of the grid where a square of size
k
can fit. If we find such a square, we immediately return its size (which isk*k
). - If no such square is found after checking all potential positions for all potential sizes, we return
0
.
Using this approach, the algorithm efficiently identifies the largest square subgrid with all 1
s on its border without checking every possible subgrid exhaustively, thus reducing the time complexity significantly compared to a naive approach.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation of the solution employs dynamic programming. We create two matrices called down
and right
. Both matrices have the same dimensions as the original grid. Each entry in down[i][j]
represents the number of consecutive 1
s below (and including) grid cell (i, j)
, extending all the way down to the bottom of the grid or until a 0
is encountered. Similarly, right[i][j]
represents the number of consecutive 1
s to the right (and including) grid cell (i, j)
, extending all the way to the right side of the grid or until a 0
is encountered.
Here is how the implementation approach breaks down:
-
Iterate over the grid from the bottom-right corner to the top-left corner. This iteration order is critical because it allows us to build the
down
andright
matrices by looking at the cells that have already been processed. -
For each cell
(i, j)
, we check if it contains a1
. If it does, we setdown[i][j]
todown[i + 1][j] + 1
andright[i][j]
toright[i][j + 1] + 1
. If the cell is at the last row or column, we simply setdown[i][j]
orright[i][j]
to1
. This is because there are no cells below or to the right to continue the consecutive sequence. -
Once the
down
andright
matrices are filled, we iterate over all potential squares starting from the largest possible sizek
and decrease the size until we either find a valid square or finish checking all sizes. -
We look for a square starting at cell
(i, j)
with sizek
. To verify if the square's borders consist entirely of1
s, we check four conditions:down[i][j]
should be greater than or equal tok
.right[i][j]
should be greater than or equal tok
.right[i + k - 1][j]
(bottom-left corner of the square) should be greater than or equal tok
.down[i][j + k - 1]
(top-right corner of the square) should be greater than or equal tok
.
-
If a square satisfying these conditions is found, we immediately return the size of the square,
k * k
, since we are iterating from the largest to the smallest possible size, ensuring that the first valid square found is also the largest. -
If no valid square is found after checking all possibilities, which means no square with borders made entirely of
1
s exists, we return0
.
This approach uses the dynamic programming paradigm to build solutions to subproblems (the number of consecutive 1
s extending downward and rightward from each cell) and uses those solutions to solve the bigger problem (finding the largest square with 1
s on its border).
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example grid:
1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1
Following the steps of the implementation approach:
-
Iterate over the grid from the bottom-right corner to the top-left corner.
We start from the bottom-right cell
(4, 4)
of the grid and move towards the top-left cell(0, 0)
, calculating thedown
andright
values for each cell. -
For each cell
(i, j)
, we check if it contains a1
.For instance, at cell
(4, 4)
, it contains a1
. Since it's in the last row and last column,down[4][4]
andright[4][4]
will both be set to1
. -
Fill the
down
andright
matrices.Here's what the matrices look like after being filled based on step 2:
down
matrix:1 3 3 1 0 1 2 2 1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1
right
matrix:4 3 2 1 0 4 3 2 2 1 3 2 0 2 1 2 1 1 1 1 1 1 1 1 1
-
Iterate over all potential squares
Starting with
k = 4
(the largest possible square based on the smallest grid dimension), check if a square of that size can exist beginning from each cell(i, j)
.From the
right
anddown
matrices, we can see that there's no cell that has bothdown
andright
values of4
. The maximum value is3
, so we can't form a square of size4
.Decrease
k
to3
and repeat the process. Now we have multiple cells with bothdown
andright
values of at least3
. We choose one, say cell(1, 1)
, and check the conditions for the square's bottom-left and top-right corners as well. Since the bottom-left(3, 1)
has adown
value of1
, which is less than3
, this cell does not form a valid square of size3
.We continue to check other potential top-left corners of squares of size
3
, eventually finding that cell(0, 1)
satisfies all conditions:down[0][1] >= 3
right[0][1] >= 3
right[0 + 3 - 1][1] (bottom-left corner) = right[2][1] >= 3
down[0][1 + 3 - 1] (top-right corner) = down[0][3] >= 3
-
Return the size of the found square
Since we've found a valid square of size
3
whose all border cells are1
, we return the size of this square:3 * 3
which is9
. -
If no valid square is found
However, if we were unable to find a valid square of size
3
, we would decreasek
to2
, then to1
, and if still no valid square is found, we would return0
. In this example, we found a square, so we return9
.
Through this process, we efficiently determined the largest square subgrid with all 1
s on its border without needing to inspect every possible subgrid. This dynamic programming approach reduces the time complexity and provides a methodical way to find the solution.
Solution Implementation
1class Solution:
2 def largest1BorderedSquare(self, grid: List[List[int]]) -> int:
3 # Get the dimensions of the grid
4 rows, cols = len(grid), len(grid[0])
5
6 # Initialize dynamic programming tables to store the count
7 # of continuous '1's until the current cell from the top and from the left.
8 down = [[0] * cols for _ in range(rows)]
9 right = [[0] * cols for _ in range(rows)]
10
11 # Fill in the tables with the count of '1's
12 for row in range(rows - 1, -1, -1):
13 for col in range(cols - 1, -1, -1):
14 if grid[row][col] == 1:
15 # If on the bottom or right edge, count is 1, otherwise add from accumulator
16 down[row][col] = 1 + (down[row + 1][col] if row + 1 < rows else 0)
17 right[row][col] = 1 + (right[row][col + 1] if col + 1 < cols else 0)
18
19 # Iterate from the largest possible square to the smallest
20 for max_side in range(min(rows, cols), 0, -1):
21 # Check every possible position for the top-left corner of the square
22 for row in range(rows - max_side + 1):
23 for col in range(cols - max_side + 1):
24 # Check if there are enough '1's to form the sides of the square
25 if (down[row][col] >= max_side and
26 right[row][col] >= max_side and
27 right[row + max_side - 1][col] >= max_side and
28 down[row][col + max_side - 1] >= max_side):
29 # If a square is found, return its area
30 return max_side * max_side
31
32 # Return 0 if no 1-bordered square is found
33 return 0
34
1class Solution {
2 public int largest1BorderedSquare(int[][] grid) {
3 int rows = grid.length; // the number of rows in the grid
4 int cols = grid[0].length; // the number of columns in the grid
5 int[][] bottom = new int[rows][cols]; // DP matrix to store consecutive 1's from current cell to bottom
6 int[][] right = new int[rows][cols]; // DP matrix to store consecutive 1's from current cell to right
7
8 // Populate the bottom and right matrix with consecutive counts of 1's
9 for (int i = rows - 1; i >= 0; --i) {
10 for (int j = cols - 1; j >= 0; --j) {
11 if (grid[i][j] == 1) { // If the cell has a 1, calculate the consecutive 1's
12 bottom[i][j] = (i + 1 < rows) ? bottom[i + 1][j] + 1 : 1;
13 right[i][j] = (j + 1 < cols) ? right[i][j + 1] + 1 : 1;
14 }
15 }
16 }
17
18 // Iterate through all possible square sizes from large to small
19 for (int size = Math.min(rows, cols); size > 0; --size) {
20 // Check for a square with the current size
21 for (int i = 0; i <= rows - size; ++i) {
22 for (int j = 0; j <= cols - size; ++j) {
23 // Verify if the current position can form a square of given size
24 if (bottom[i][j] >= size && right[i][j] >= size &&
25 right[i + size - 1][j] >= size && bottom[i][j + size - 1] >= size) {
26 return size * size; // Return the area of the square
27 }
28 }
29 }
30 }
31
32 return 0; // if no 1-bordered square is found, return 0
33 }
34}
35
1#include <vector>
2#include <algorithm>
3#include <cstring>
4
5class Solution {
6public:
7 int largest1BorderedSquare(std::vector<std::vector<int>>& grid) {
8 int rows = grid.size(), cols = grid[0].size();
9
10 // Create 2D arrays to store the count of continuous 1's in the down and right directions
11 int down[rows][cols];
12 int right[rows][cols];
13 // Initialize the arrays with zeros
14 std::memset(down, 0, sizeof(down));
15 std::memset(right, 0, sizeof(right));
16
17 // Fill the down and right arrays with the consecutive 1's count
18 for (int i = rows - 1; i >= 0; --i) {
19 for (int j = cols - 1; j >= 0; --j) {
20 if (grid[i][j] == 1) {
21 down[i][j] = (i + 1 < rows) ? down[i + 1][j] + 1 : 1;
22 right[i][j] = (j + 1 < cols) ? right[i][j + 1] + 1 : 1;
23 }
24 }
25 }
26
27 // Loop from the maximum possible size of the square to the smallest
28 for (int maxSize = std::min(rows, cols); maxSize > 0; --maxSize) {
29 // Check each possible position for the top-left corner of the square
30 for (int i = 0; i <= rows - maxSize; ++i) {
31 for (int j = 0; j <= cols - maxSize; ++j) {
32 // Check if both vertical and horizontal sides have at least 'maxSize' 1's
33 if (down[i][j] >= maxSize && right[i][j] >= maxSize &&
34 right[i + maxSize - 1][j] >= maxSize &&
35 down[i][j + maxSize - 1] >= maxSize) {
36 // If a valid square is found, return its area
37 return maxSize * maxSize;
38 }
39 }
40 }
41 }
42
43 // Return 0 if no 1-bordered square is found
44 return 0;
45 }
46};
47
1function largest1BorderedSquare(grid: number[][]): number {
2 const rows = grid.length;
3 const cols = grid[0].length;
4
5 // Create 2D arrays to store the count of continuous 1s in the downward and rightward directions
6 const down: number[][] = Array.from({ length: rows }, () => new Array(cols).fill(0));
7 const right: number[][] = Array.from({ length: rows }, () => new Array(cols).fill(0));
8
9 // Fill the down and right arrays with the consecutive 1s count
10 for (let i = rows - 1; i >= 0; i--) {
11 for (let j = cols - 1; j >= 0; j--) {
12 if (grid[i][j] === 1) {
13 down[i][j] = (i + 1 < rows) ? down[i + 1][j] + 1 : 1;
14 right[i][j] = (j + 1 < cols) ? right[i][j + 1] + 1 : 1;
15 }
16 }
17 }
18
19 // Check for the largest possible square by looping from the maximum possible size to the smallest
20 for (let maxSize = Math.min(rows, cols); maxSize > 0; maxSize--) {
21 for (let i = 0; i <= rows - maxSize; i++) {
22 for (let j = 0; j <= cols - maxSize; j++) {
23 // Validate if both vertical and horizontal sides have at least 'maxSize' 1s
24 if (down[i][j] >= maxSize && right[i][j] >= maxSize &&
25 right[i + maxSize - 1][j] >= maxSize &&
26 down[i][j + maxSize - 1] >= maxSize) {
27 // If a valid square is found, return its area
28 return maxSize * maxSize;
29 }
30 }
31 }
32 }
33
34 // Return 0 if no 1-bordered square is found
35 return 0;
36}
37
38// Usage
39const grid = [
40 [1, 1, 1],
41 [1, 0, 1],
42 [1, 1, 1]
43];
44const largestSquare = largest1BorderedSquare(grid);
45console.log(largestSquare); // Outputs the area of the largest 1-bordered square
46
Time and Space Complexity
The given Python code is for finding the largest 1-bounded square in a 2D binary grid.
Time Complexity:
The time complexity of the code can be analyzed as follows:
- First, we have two nested loops that iterate over the entire grid of size
m * n
to populate thedown
andright
matrices. This step has a complexity ofO(m * n)
. - Next, we have a triple nested loop where the outer loop decreases
k
frommin(m, n)
to 1, and the two inner loops iterate over them
andn
dimensions. In the worst case wherek
equalsmin(m, n)
, the complexity isO(m * n * min(m, n))
.
Therefore, the overall time complexity is O(m * n) + O(m * n * min(m, n)) => O(m * n * min(m, n))
, which is dominated by the triple nested loop.
Space Complexity:
The space complexity can be analyzed by considering the additional space used:
- Two auxiliary matrices
down
andright
, each of sizem * n
, are created. Hence, the space required for these matrices is2 * m * n
. - Apart from these two matrices, only a constant amount of additional space is used for indices and temporary variables.
Thus, the overall space complexity is O(m * n)
because the space used for the down
and right
matrices dominates the space requirement.
Learn more about how to find time and space complexity quickly using problem constraints.
In a binary min heap, the minimum element can be found in:
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!