1139. Largest 1-Bordered Square
Problem Description
You are given a 2D grid containing only 0s and 1s. Your task is to find the largest square subgrid where all the cells on the border of the square contain 1s. The interior cells can be either 0 or 1 - only the border matters.
For example, if you have a 3×3 square, you need all 8 border cells (the outer frame) to be 1s to consider it valid. The center cell can be anything.
The problem asks you to return the number of elements in the largest such square (which is the area, or side length squared). If no valid square with all 1s on its border exists in the grid, return 0.
Key points to understand:
- You're looking for a square subgrid (not rectangle)
- Only the border cells of the square need to be
1s - The interior cells don't matter - they can be
0or1 - You want the largest possible such square
- Return the total number of cells in that square (side × side)
For instance, a valid 3×3 square bordered by 1s would return 9 (since 3 × 3 = 9).
Intuition
To check if a square has all 1s on its border, we need to verify four sides: top, bottom, left, and right. A naive approach would be to check each cell on the border for every possible square, but this would be inefficient.
The key insight is that we can precompute useful information to speed up our border checks. For any position (i, j) in the grid, if we know:
- How many consecutive
1s extend downward from this position - How many consecutive
1s extend to the right from this position
Then we can quickly verify if a square's border is valid.
Why does this help? Consider a square with top-left corner at (i, j) and side length k:
- The left side of the square needs
kconsecutive1s going down from(i, j)- we can check ifdown[i][j] >= k - The top side needs
kconsecutive1s going right from(i, j)- we can check ifright[i][j] >= k - The right side needs
kconsecutive1s going down from(i, j+k-1)- we can check ifdown[i][j+k-1] >= k - The bottom side needs
kconsecutive1s going right from(i+k-1, j)- we can check ifright[i+k-1][j] >= k
By preprocessing these down and right arrays using dynamic programming (working backwards from bottom-right), we can check any square's validity in O(1) time.
Since we want the largest square, we can enumerate side lengths from largest to smallest. As soon as we find a valid square of side length k, we can immediately return k * k since any larger square would have been found earlier in our search.
Learn more about Dynamic Programming patterns.
Solution Implementation
1class Solution: 2 def largest1BorderedSquare(self, grid: List[List[int]]) -> int: 3 # Get dimensions of the grid 4 rows, cols = len(grid), len(grid[0]) 5 6 # Create DP tables to store consecutive 1s count 7 # consecutive_ones_down[i][j] = number of consecutive 1s going down from (i,j) 8 # consecutive_ones_right[i][j] = number of consecutive 1s going right from (i,j) 9 consecutive_ones_down = [[0] * cols for _ in range(rows)] 10 consecutive_ones_right = [[0] * cols for _ in range(rows)] 11 12 # Build the DP tables by iterating from bottom-right to top-left 13 for row in range(rows - 1, -1, -1): 14 for col in range(cols - 1, -1, -1): 15 if grid[row][col] == 1: 16 # Count consecutive 1s going down from current position 17 if row + 1 < rows: 18 consecutive_ones_down[row][col] = consecutive_ones_down[row + 1][col] + 1 19 else: 20 consecutive_ones_down[row][col] = 1 21 22 # Count consecutive 1s going right from current position 23 if col + 1 < cols: 24 consecutive_ones_right[row][col] = consecutive_ones_right[row][col + 1] + 1 25 else: 26 consecutive_ones_right[row][col] = 1 27 28 # Try to find the largest square, starting from maximum possible size 29 max_possible_size = min(rows, cols) 30 31 for square_size in range(max_possible_size, 0, -1): 32 # Check all possible top-left corners for current square size 33 for top_row in range(rows - square_size + 1): 34 for left_col in range(cols - square_size + 1): 35 # Calculate positions of the four corners 36 bottom_row = top_row + square_size - 1 37 right_col = left_col + square_size - 1 38 39 # Check if all four borders have enough consecutive 1s 40 has_top_border = consecutive_ones_right[top_row][left_col] >= square_size 41 has_left_border = consecutive_ones_down[top_row][left_col] >= square_size 42 has_bottom_border = consecutive_ones_right[bottom_row][left_col] >= square_size 43 has_right_border = consecutive_ones_down[top_row][right_col] >= square_size 44 45 if has_top_border and has_left_border and has_bottom_border and has_right_border: 46 # Found a valid square with all borders made of 1s 47 return square_size * square_size 48 49 # No valid square found 50 return 0 511class Solution { 2 public int largest1BorderedSquare(int[][] grid) { 3 int rows = grid.length; 4 int cols = grid[0].length; 5 6 // dp arrays to store consecutive 1s count 7 // downCount[i][j] = number of consecutive 1s going down from position (i,j) 8 // rightCount[i][j] = number of consecutive 1s going right from position (i,j) 9 int[][] downCount = new int[rows][cols]; 10 int[][] rightCount = new int[rows][cols]; 11 12 // Build the dp arrays from bottom-right to top-left 13 for (int i = rows - 1; i >= 0; i--) { 14 for (int j = cols - 1; j >= 0; j--) { 15 if (grid[i][j] == 1) { 16 // Calculate consecutive 1s going down 17 downCount[i][j] = (i + 1 < rows) ? downCount[i + 1][j] + 1 : 1; 18 19 // Calculate consecutive 1s going right 20 rightCount[i][j] = (j + 1 < cols) ? rightCount[i][j + 1] + 1 : 1; 21 } 22 } 23 } 24 25 // Try all possible square sizes from largest to smallest 26 int maxPossibleSize = Math.min(rows, cols); 27 for (int squareSize = maxPossibleSize; squareSize > 0; squareSize--) { 28 // Check all possible top-left corners for current square size 29 for (int topRow = 0; topRow <= rows - squareSize; topRow++) { 30 for (int leftCol = 0; leftCol <= cols - squareSize; leftCol++) { 31 // Verify if a valid bordered square exists at this position 32 // Check: 1. Top-left corner has enough 1s going down (left border) 33 // 2. Top-left corner has enough 1s going right (top border) 34 // 3. Bottom-left corner has enough 1s going right (bottom border) 35 // 4. Top-right corner has enough 1s going down (right border) 36 boolean hasValidTopBorder = rightCount[topRow][leftCol] >= squareSize; 37 boolean hasValidLeftBorder = downCount[topRow][leftCol] >= squareSize; 38 boolean hasValidBottomBorder = rightCount[topRow + squareSize - 1][leftCol] >= squareSize; 39 boolean hasValidRightBorder = downCount[topRow][leftCol + squareSize - 1] >= squareSize; 40 41 if (hasValidTopBorder && hasValidLeftBorder && 42 hasValidBottomBorder && hasValidRightBorder) { 43 // Found the largest valid square, return its area 44 return squareSize * squareSize; 45 } 46 } 47 } 48 } 49 50 // No valid bordered square found 51 return 0; 52 } 53} 541class Solution { 2public: 3 int largest1BorderedSquare(vector<vector<int>>& grid) { 4 int rows = grid.size(); 5 int cols = grid[0].size(); 6 7 // dp arrays to store consecutive 1s count 8 // consecutiveDown[i][j]: number of consecutive 1s going down from (i,j) 9 // consecutiveRight[i][j]: number of consecutive 1s going right from (i,j) 10 int consecutiveDown[rows][cols]; 11 int consecutiveRight[rows][cols]; 12 13 // Initialize arrays with 0 14 memset(consecutiveDown, 0, sizeof(consecutiveDown)); 15 memset(consecutiveRight, 0, sizeof(consecutiveRight)); 16 17 // Build the consecutive count arrays from bottom-right to top-left 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 // Count consecutive 1s going down from current position 22 consecutiveDown[i][j] = (i + 1 < rows) ? consecutiveDown[i + 1][j] + 1 : 1; 23 24 // Count consecutive 1s going right from current position 25 consecutiveRight[i][j] = (j + 1 < cols) ? consecutiveRight[i][j + 1] + 1 : 1; 26 } 27 } 28 } 29 30 // Try all possible square sizes from largest to smallest 31 int maxPossibleSize = min(rows, cols); 32 for (int squareSize = maxPossibleSize; squareSize > 0; --squareSize) { 33 // Try all possible top-left corners for current square size 34 for (int topRow = 0; topRow <= rows - squareSize; ++topRow) { 35 for (int leftCol = 0; leftCol <= cols - squareSize; ++leftCol) { 36 // Check if all four borders have enough consecutive 1s 37 // Top-left corner needs at least squareSize 1s going down and right 38 bool topLeftValid = consecutiveDown[topRow][leftCol] >= squareSize && 39 consecutiveRight[topRow][leftCol] >= squareSize; 40 41 // Bottom-left corner needs at least squareSize 1s going right 42 bool bottomLeftValid = consecutiveRight[topRow + squareSize - 1][leftCol] >= squareSize; 43 44 // Top-right corner needs at least squareSize 1s going down 45 bool topRightValid = consecutiveDown[topRow][leftCol + squareSize - 1] >= squareSize; 46 47 if (topLeftValid && bottomLeftValid && topRightValid) { 48 // Found valid square with all borders made of 1s 49 return squareSize * squareSize; 50 } 51 } 52 } 53 } 54 55 // No valid square found 56 return 0; 57 } 58}; 591function largest1BorderedSquare(grid: number[][]): number { 2 const rows: number = grid.length; 3 const cols: number = grid[0].length; 4 5 // DP arrays to store consecutive 1s count 6 // consecutiveDown[i][j]: number of consecutive 1s going down from (i,j) 7 // consecutiveRight[i][j]: number of consecutive 1s going right from (i,j) 8 const consecutiveDown: number[][] = Array(rows).fill(0).map(() => Array(cols).fill(0)); 9 const consecutiveRight: number[][] = Array(rows).fill(0).map(() => Array(cols).fill(0)); 10 11 // Build the consecutive count arrays from bottom-right to top-left 12 // This allows us to count consecutive 1s in each direction efficiently 13 for (let i = rows - 1; i >= 0; i--) { 14 for (let j = cols - 1; j >= 0; j--) { 15 if (grid[i][j] === 1) { 16 // Count consecutive 1s going down from current position 17 // If there's a cell below, add 1 to its down count, otherwise this is the start (count = 1) 18 consecutiveDown[i][j] = (i + 1 < rows) ? consecutiveDown[i + 1][j] + 1 : 1; 19 20 // Count consecutive 1s going right from current position 21 // If there's a cell to the right, add 1 to its right count, otherwise this is the start (count = 1) 22 consecutiveRight[i][j] = (j + 1 < cols) ? consecutiveRight[i][j + 1] + 1 : 1; 23 } 24 } 25 } 26 27 // Try all possible square sizes from largest to smallest 28 // This ensures we find the maximum area as soon as possible 29 const maxPossibleSize: number = Math.min(rows, cols); 30 31 for (let squareSize = maxPossibleSize; squareSize > 0; squareSize--) { 32 // Try all possible top-left corners for current square size 33 for (let topRow = 0; topRow <= rows - squareSize; topRow++) { 34 for (let leftCol = 0; leftCol <= cols - squareSize; leftCol++) { 35 // Check if all four borders have enough consecutive 1s 36 37 // Top-left corner needs at least squareSize 1s going down and right 38 // This forms the top border (going right) and left border (going down) 39 const topLeftValid: boolean = consecutiveDown[topRow][leftCol] >= squareSize && 40 consecutiveRight[topRow][leftCol] >= squareSize; 41 42 // Bottom-left corner needs at least squareSize 1s going right 43 // This forms the bottom border 44 const bottomLeftValid: boolean = consecutiveRight[topRow + squareSize - 1][leftCol] >= squareSize; 45 46 // Top-right corner needs at least squareSize 1s going down 47 // This forms the right border 48 const topRightValid: boolean = consecutiveDown[topRow][leftCol + squareSize - 1] >= squareSize; 49 50 if (topLeftValid && bottomLeftValid && topRightValid) { 51 // Found valid square with all borders made of 1s 52 // Return the area of the square 53 return squareSize * squareSize; 54 } 55 } 56 } 57 } 58 59 // No valid square found 60 return 0; 61} 62Solution Approach
The solution uses prefix sum preprocessing combined with enumeration to efficiently find the largest square with all 1s on its border.
Step 1: Preprocess the grid with prefix sums
We create two 2D arrays:
down[i][j]: counts consecutive1s going downward from position(i, j)right[i][j]: counts consecutive1s going to the right from position(i, j)
We fill these arrays by iterating from bottom-right to top-left:
for i in range(m - 1, -1, -1): for j in range(n - 1, -1, -1): if grid[i][j]: down[i][j] = down[i + 1][j] + 1 if i + 1 < m else 1 right[i][j] = right[i][j + 1] + 1 if j + 1 < n else 1
For each cell with value 1:
down[i][j]equals1 + down[i+1][j](or1if at the bottom edge)right[i][j]equals1 + right[i][j+1](or1if at the right edge)
Step 2: Enumerate possible square sizes
We try square sizes from largest to smallest (min(m, n) down to 1):
for k in range(min(m, n), 0, -1):
Step 3: Check all possible positions for each square size
For each square size k, we check all valid top-left corner positions (i, j):
for i in range(m - k + 1): for j in range(n - k + 1):
Step 4: Validate the square's border
A square with top-left at (i, j) and side length k has valid borders if:
- Left side:
down[i][j] >= k(needskconsecutive1s going down) - Top side:
right[i][j] >= k(needskconsecutive1s going right) - Right side:
down[i][j + k - 1] >= k(from top-right corner going down) - Bottom side:
right[i + k - 1][j] >= k(from bottom-left corner going right)
If all four conditions are met, we immediately return k * k since we're checking from largest to smallest.
Time Complexity: O(m * n * min(m, n)) where preprocessing takes O(m * n) and checking all squares takes O(m * n * min(m, n)).
Space Complexity: O(m * n) for the two auxiliary arrays.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the largest square with all 1s on its border using this grid:
grid = [[1, 1, 1], [1, 0, 1], [1, 1, 1]]
Step 1: Build the preprocessing arrays
We create down and right arrays by working backwards from bottom-right to top-left.
Starting with down (counts consecutive 1s going downward):
down[2][2] = 1(bottom-right, only itself)down[2][1] = 1(grid[2][1] = 1, but it's the bottom row)down[2][0] = 1(bottom-left, only itself)down[1][2] = 1 + down[2][2] = 2(grid[1][2] = 1, adds to below)down[1][1] = 0(grid[1][1] = 0, breaks the chain)down[1][0] = 1 + down[2][0] = 2(grid[1][0] = 1, adds to below)down[0][2] = 1 + down[1][2] = 3(all three cells going down are 1s)down[0][1] = 1(grid[0][1] = 1, but grid[1][1] = 0 below)down[0][0] = 1 + down[1][0] = 3(all three cells going down are 1s)
down = [[3, 1, 3], [2, 0, 2], [1, 1, 1]]
Similarly for right (counts consecutive 1s going rightward):
right = [[3, 2, 1], [1, 0, 1], [3, 2, 1]]
Step 2: Check squares from largest to smallest
Starting with size k=3 (the maximum possible):
- Check position (0,0) for a 3×3 square:
- Left side:
down[0][0] = 3 >= 3✓ - Top side:
right[0][0] = 3 >= 3✓ - Right side:
down[0][2] = 3 >= 3✓ - Bottom side:
right[2][0] = 3 >= 3✓
- Left side:
All four borders are valid! We found a 3×3 square with all 1s on its border.
Return: 3 × 3 = 9
Note that the center cell grid[1][1] = 0 doesn't matter - only the 8 border cells need to be 1s, which they are:
Border cells: [1, 1, 1] [1, _, 1] [1, 1, 1]
If this 3×3 square hadn't worked, we would continue checking smaller sizes (k=2, then k=1) until finding a valid square or returning 0 if none exist.
Time and Space Complexity
Time Complexity: O(m × n × min(m, n))
The algorithm consists of two main parts:
-
Preprocessing phase: Computing the
downandrightarrays requires iterating through all cells in the grid once, which takesO(m × n)time. -
Search phase: The algorithm searches for the largest square by:
- Iterating through all possible square sizes
kfrommin(m, n)down to 1, which gives usO(min(m, n))iterations - For each square size
k, checking all possible top-left positions(i, j)where a square of sizekcould fit, which requiresO((m - k + 1) × (n - k + 1))iterations - Each validity check for a square takes
O(1)time
In the worst case, when no valid square is found until
k = 1, the total iterations would be:Σ(k=1 to min(m,n)) (m - k + 1) × (n - k + 1) ≈ O(m × n × min(m, n)) - Iterating through all possible square sizes
Therefore, the overall time complexity is O(m × n × min(m, n)).
Space Complexity: O(m × n)
The algorithm uses two auxiliary 2D arrays:
down[m][n]: stores the count of consecutive 1s going downward from each cellright[m][n]: stores the count of consecutive 1s going rightward from each cell
Both arrays have dimensions m × n, resulting in a space complexity of O(m × n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Border Validation with Full Square Validation
A common mistake is thinking that checking the four corners' consecutive counts validates the entire border. However, the consecutive count arrays tell us about potential borders, not guaranteed complete borders.
The Pitfall: When we check consecutive_ones_right[top_row][left_col] >= square_size, we're verifying that there are enough consecutive 1s going right from the top-left corner. But this doesn't guarantee that ALL cells on the top border are part of our square - some of those consecutive 1s might extend beyond our square's boundary.
Why This Works Anyway: The algorithm is correct because:
- We check all four corners' consecutive counts
consecutive_ones_right[top_row][left_col] >= square_sizeensures the top border has at leastsquare_sizeconsecutive 1s starting from the left cornerconsecutive_ones_down[top_row][right_col] >= square_sizeensures the right border has at leastsquare_sizeconsecutive 1s starting from the top-right corner- The combination of all four checks guarantees the entire border is made of 1s
2. Off-by-One Errors in Index Calculations
The Pitfall: It's easy to make mistakes when calculating the bottom-right corner indices:
# Incorrect: bottom_row = top_row + square_size # This would be one row too far right_col = left_col + square_size # This would be one column too far # Correct: bottom_row = top_row + square_size - 1 right_col = left_col + square_size - 1
Solution: Remember that if the top-left corner is at (i, j) and the square has side length k, then:
- Top-right corner:
(i, j + k - 1) - Bottom-left corner:
(i + k - 1, j) - Bottom-right corner:
(i + k - 1, j + k - 1)
3. Incorrect Preprocessing Direction
The Pitfall: Building the prefix arrays in the wrong direction:
# Incorrect - going from top-left to bottom-right: for row in range(rows): for col in range(cols): if grid[row][col] == 1: consecutive_ones_down[row][col] = consecutive_ones_down[row - 1][col] + 1 # Wrong!
Solution: The arrays must be built from bottom-right to top-left because:
consecutive_ones_down[i][j]needs to know about cells below itconsecutive_ones_right[i][j]needs to know about cells to its right- We can only compute these if we've already processed those cells
4. Not Handling Edge Cases in Preprocessing
The Pitfall: Forgetting to handle boundary conditions when building prefix arrays:
# Incorrect - might cause index out of bounds: consecutive_ones_down[row][col] = consecutive_ones_down[row + 1][col] + 1 # Correct - with boundary check: if row + 1 < rows: consecutive_ones_down[row][col] = consecutive_ones_down[row + 1][col] + 1 else: consecutive_ones_down[row][col] = 1
Solution: Always check if the next cell exists before accessing it. For cells on the edge, initialize the count to 1 if the cell itself contains a 1.
What's the output of running the following function using the following tree as input? 
1def serialize(root): 2 res = [] 3 def dfs(root): 4 if not root: 5 res.append('x') 6 return 7 res.append(root.val) 8 dfs(root.left) 9 dfs(root.right) 10 dfs(root) 11 return ' '.join(res) 121import java.util.StringJoiner; 2 3public static String serialize(Node root) { 4 StringJoiner res = new StringJoiner(" "); 5 serializeDFS(root, res); 6 return res.toString(); 7} 8 9private static void serializeDFS(Node root, StringJoiner result) { 10 if (root == null) { 11 result.add("x"); 12 return; 13 } 14 result.add(Integer.toString(root.val)); 15 serializeDFS(root.left, result); 16 serializeDFS(root.right, result); 17} 181function serialize(root) { 2 let res = []; 3 serialize_dfs(root, res); 4 return res.join(" "); 5} 6 7function serialize_dfs(root, res) { 8 if (!root) { 9 res.push("x"); 10 return; 11 } 12 res.push(root.val); 13 serialize_dfs(root.left, res); 14 serialize_dfs(root.right, res); 15} 16Recommended 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
Coding Interview 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
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way 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 assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!