85. Maximal Rectangle
Problem Description
You are given a 2D binary matrix of size rows x cols
where each cell contains either '0' or '1'. Your task is to find the largest rectangle that contains only '1's and return its area.
A rectangle in the matrix is defined by its corners and must be axis-aligned (sides parallel to the matrix edges). The rectangle can only include cells that contain '1'.
For example, if you have a matrix like:
1 0 1 0 0 1 0 1 1 1 1 1 1 1 1 1 0 0 1 0
The largest rectangle containing only 1's has an area of 6 (formed by the '1's in the second and third rows, from columns 2 to 4).
The solution works by transforming this problem into multiple "Largest Rectangle in Histogram" problems. For each row, it treats that row as the base and calculates the height of consecutive '1's above (including the current row). This creates a histogram for each row, where the height represents how many consecutive '1's extend upward from that position.
The algorithm:
- For each row, build a histogram where each bar's height represents the number of consecutive '1's from that row going upward
- If a cell contains '0', the height resets to 0
- If a cell contains '1', the height increases by 1 from the previous row's height at that position
- For each row's histogram, calculate the largest rectangle area using a monotonic stack approach
- Track the maximum area found across all rows
The largestRectangleArea
helper function uses a monotonic stack to find the largest rectangle in a histogram by determining, for each bar, the furthest left and right positions where the height is at least as tall as the current bar.
Intuition
The key insight is recognizing that this problem can be reduced to a simpler, well-known problem: finding the largest rectangle in a histogram.
Imagine looking at the matrix from the side. If we fix a particular row as our "base," we can count how many consecutive '1's extend upward from each position in that row. This gives us a histogram where each bar's height represents the vertical stretch of '1's at that column.
For instance, consider this small matrix:
1 0 1 1 1 1
If we use the second row as our base:
- Column 0: has 2 consecutive '1's going up (including current row) → height = 2
- Column 1: has 1 consecutive '1' (just the current row, since above is '0') → height = 1
- Column 2: has 2 consecutive '1's going up → height = 2
This gives us a histogram with heights [2, 1, 2]
.
The brilliant part is that any rectangle in the original matrix that ends at a particular row can be represented as a rectangle in that row's histogram. Why? Because:
- The width of the rectangle in the histogram corresponds to the width in the matrix
- The height of the rectangle in the histogram represents how many rows up the rectangle extends
By processing each row and treating it as a potential bottom edge of our rectangle, we ensure we consider all possible rectangles in the matrix. The heights array is built incrementally: when we encounter a '1', we add to the previous height (extending the bar upward), and when we encounter a '0', we reset to 0 (breaking the consecutive chain).
Once we have the histogram for each row, we can apply the classic "largest rectangle in histogram" algorithm using a monotonic stack. This algorithm efficiently finds, for each bar, how far left and right we can extend while maintaining at least that bar's height, allowing us to calculate the maximum possible rectangle area for each bar as a potential minimum height.
The maximum area across all these histograms (one per row) gives us our answer.
Learn more about Stack, Dynamic Programming and Monotonic Stack patterns.
Solution Approach
The solution uses a monotonic stack approach combined with dynamic histogram construction. Let's walk through the implementation step by step:
Step 1: Build Heights Array Dynamically
For each row in the matrix, we maintain a heights
array where heights[j]
represents the number of consecutive '1's extending upward from the current row at column j
.
heights = [0] * len(matrix[0]) # Initialize heights for all columns
for row in matrix:
for j, v in enumerate(row):
if v == "1":
heights[j] += 1 # Extend the bar upward
else:
heights[j] = 0 # Reset to 0 when we hit a '0'
Step 2: Find Largest Rectangle in Histogram
For each row's histogram, we use the monotonic stack algorithm to find the largest rectangle. This involves finding, for each bar, the nearest smaller bars on both left and right sides.
Finding Left Boundaries:
left = [-1] * n # leftmost position where height >= current
stk = []
for i, h in enumerate(heights):
while stk and heights[stk[-1]] >= h:
stk.pop() # Remove bars taller than current
if stk:
left[i] = stk[-1] # Previous smaller bar's index
stk.append(i)
The stack maintains indices of bars in increasing order of height. When we encounter a bar shorter than the stack's top, we pop until we find a smaller bar or the stack is empty.
Finding Right Boundaries:
right = [n] * n # rightmost position where height >= current
stk = []
for i in range(n - 1, -1, -1): # Traverse right to left
h = heights[i]
while stk and heights[stk[-1]] >= h:
stk.pop()
if stk:
right[i] = stk[-1]
stk.append(i)
Step 3: Calculate Maximum Area
For each bar at position i
with height h
, the maximum rectangle using this bar as the minimum height is:
- Width:
right[i] - left[i] - 1
(exclusive boundaries) - Height:
h
- Area:
h * (right[i] - left[i] - 1)
max_area = max(h * (right[i] - left[i] - 1) for i, h in enumerate(heights))
Why This Works
The monotonic stack efficiently finds boundaries in O(n)
time because:
- Each element is pushed and popped at most once
- The stack maintains a sorted order, allowing us to quickly find the nearest smaller element
left[i]
gives us the rightmost position to the left where height <heights[i]
right[i]
gives us the leftmost position to the right where height <heights[i]
Overall Algorithm
- For each of the
m
rows, we:- Update the heights array in
O(n)
time - Calculate the largest rectangle in the histogram in
O(n)
time
- Update the heights array in
- Return the maximum area found across all rows
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns.
Space Complexity: O(n)
for the heights array and auxiliary arrays used in the histogram calculation.
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 a small example to illustrate the solution approach:
Input Matrix:
1 0 1 1 1 1 1 1 0 1 1 0
Step 1: Process Row 0
- Initialize heights:
[0, 0, 0, 0]
- After processing row 0:
heights = [1, 0, 1, 1]
- This represents the histogram for row 0:
1 1 1 _______ 0 1 2 3 (column indices)
- Apply largest rectangle in histogram:
- For each bar, find left and right boundaries:
- Bar 0 (h=1): left=-1, right=1, area = 1 × (1-(-1)-1) = 1
- Bar 1 (h=0): skip (height is 0)
- Bar 2 (h=1): left=1, right=4, area = 1 × (4-1-1) = 2
- Bar 3 (h=1): left=1, right=4, area = 1 × (4-1-1) = 2
- Maximum area for row 0: 2
- For each bar, find left and right boundaries:
Step 2: Process Row 1
- Update heights based on row 1:
[2, 1, 2, 2]
- Column 0: '1' → heights[0] = 1 + 1 = 2
- Column 1: '1' → heights[1] = 0 + 1 = 1
- Column 2: '1' → heights[2] = 1 + 1 = 2
- Column 3: '1' → heights[3] = 1 + 1 = 2
- This represents accumulated heights:
2 2 2 1 _______ 0 1 2 3
- Apply largest rectangle in histogram:
- Bar 0 (h=2): left=-1, right=1, area = 2 × (1-(-1)-1) = 2
- Bar 1 (h=1): left=-1, right=4, area = 1 × (4-(-1)-1) = 4
- Bar 2 (h=2): left=1, right=4, area = 2 × (4-1-1) = 4
- Bar 3 (h=2): left=1, right=4, area = 2 × (4-1-1) = 4
- Maximum area for row 1: 4 (represents the rectangle from row 0-1, columns 1-3)
Step 3: Process Row 2
- Update heights based on row 2:
[0, 2, 3, 0]
- Column 0: '0' → heights[0] = 0 (reset)
- Column 1: '1' → heights[1] = 1 + 1 = 2
- Column 2: '1' → heights[2] = 2 + 1 = 3
- Column 3: '0' → heights[3] = 0 (reset)
- This represents:
3 2 _______ 0 1 2 3
- Apply largest rectangle in histogram:
- Bar 0 (h=0): skip
- Bar 1 (h=2): left=0, right=3, area = 2 × (3-0-1) = 4
- Bar 2 (h=3): left=0, right=3, area = 3 × (3-0-1) = 6
- Bar 3 (h=0): skip
- Maximum area for row 2: 6
Wait, that gives us 6, but let's verify: the rectangle with height 3 and width 2 doesn't exist in our matrix. Let me recalculate:
- Bar 2 (h=3): left=1, right=3, area = 3 × (3-1-1) = 3
Actually, the largest rectangle would be height 2, width 2:
- Bar 1 (h=2): left=0, right=3, area = 2 × (3-0-1) = 4
Final Answer: Maximum area across all rows = 4
This rectangle corresponds to:
Row 0: [_, _, 1, 1] Row 1: [_, 1, 1, 1]
Which forms a 2×2 rectangle in the original matrix (rows 0-1, columns 1-3 gives us 4 '1's).
Solution Implementation
1class Solution:
2 def maximalRectangle(self, matrix: List[List[str]]) -> int:
3 """
4 Find the maximal rectangle containing only '1's in a binary matrix.
5
6 Strategy: Convert each row to a histogram problem where heights represent
7 consecutive '1's from top to current row, then find the largest rectangle
8 in each histogram.
9
10 Args:
11 matrix: 2D binary matrix with '0' and '1' characters
12
13 Returns:
14 Area of the largest rectangle containing only '1's
15 """
16 if not matrix or not matrix[0]:
17 return 0
18
19 # Initialize heights array for histogram representation
20 num_cols = len(matrix[0])
21 heights = [0] * num_cols
22 max_area = 0
23
24 # Process each row
25 for row in matrix:
26 # Update heights: increment if '1', reset to 0 if '0'
27 for col_idx, value in enumerate(row):
28 if value == "1":
29 heights[col_idx] += 1
30 else:
31 heights[col_idx] = 0
32
33 # Calculate max rectangle area for current histogram
34 max_area = max(max_area, self.largestRectangleArea(heights))
35
36 return max_area
37
38 def largestRectangleArea(self, heights: List[int]) -> int:
39 """
40 Find the largest rectangle area in a histogram.
41
42 Uses monotonic stack to find the nearest smaller element on both left and right
43 for each bar, then calculates the maximum rectangle area.
44
45 Args:
46 heights: List of bar heights in the histogram
47
48 Returns:
49 Area of the largest rectangle in the histogram
50 """
51 n = len(heights)
52 if n == 0:
53 return 0
54
55 # Arrays to store indices of nearest smaller elements
56 left_boundaries = [-1] * n # Index of nearest smaller element on the left
57 right_boundaries = [n] * n # Index of nearest smaller element on the right
58
59 # Find left boundaries using monotonic increasing stack
60 stack = []
61 for i, height in enumerate(heights):
62 # Pop elements >= current height
63 while stack and heights[stack[-1]] >= height:
64 stack.pop()
65
66 # If stack not empty, top element is the nearest smaller on left
67 if stack:
68 left_boundaries[i] = stack[-1]
69
70 stack.append(i)
71
72 # Find right boundaries using monotonic increasing stack (traverse right to left)
73 stack = []
74 for i in range(n - 1, -1, -1):
75 height = heights[i]
76
77 # Pop elements >= current height
78 while stack and heights[stack[-1]] >= height:
79 stack.pop()
80
81 # If stack not empty, top element is the nearest smaller on right
82 if stack:
83 right_boundaries[i] = stack[-1]
84
85 stack.append(i)
86
87 # Calculate maximum rectangle area
88 # For each bar, the rectangle extends from (left_boundary + 1) to (right_boundary - 1)
89 max_area = max(
90 height * (right_boundaries[i] - left_boundaries[i] - 1)
91 for i, height in enumerate(heights)
92 )
93
94 return max_area
95
1class Solution {
2 /**
3 * Finds the largest rectangle containing only 1's in a binary matrix.
4 * Uses dynamic programming to build histogram heights for each row,
5 * then finds the largest rectangle in each histogram.
6 *
7 * @param matrix 2D char array containing '0's and '1's
8 * @return area of the largest rectangle containing only 1's
9 */
10 public int maximalRectangle(char[][] matrix) {
11 int numCols = matrix[0].length;
12 // Heights array represents histogram heights at current row
13 int[] heights = new int[numCols];
14 int maxArea = 0;
15
16 // Process each row to build cumulative histogram
17 for (char[] currentRow : matrix) {
18 for (int col = 0; col < numCols; col++) {
19 if (currentRow[col] == '1') {
20 // Increment height if current cell is '1'
21 heights[col] += 1;
22 } else {
23 // Reset height to 0 if current cell is '0'
24 heights[col] = 0;
25 }
26 }
27 // Calculate max rectangle area for current histogram
28 maxArea = Math.max(maxArea, largestRectangleArea(heights));
29 }
30
31 return maxArea;
32 }
33
34 /**
35 * Calculates the largest rectangle area in a histogram.
36 * Uses monotonic stack to find left and right boundaries for each bar.
37 *
38 * @param heights array representing histogram bar heights
39 * @return area of the largest rectangle in the histogram
40 */
41 private int largestRectangleArea(int[] heights) {
42 int maxArea = 0;
43 int n = heights.length;
44 Deque<Integer> stack = new ArrayDeque<>();
45
46 // Arrays to store left and right boundaries for each bar
47 int[] leftBoundary = new int[n]; // Index of nearest smaller element on left
48 int[] rightBoundary = new int[n]; // Index of nearest smaller element on right
49
50 // Initialize right boundaries to n (beyond last index)
51 Arrays.fill(rightBoundary, n);
52
53 // Single pass to find both left and right boundaries
54 for (int i = 0; i < n; i++) {
55 // Pop elements from stack that are >= current height
56 // These elements have found their right boundary (current index)
57 while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
58 rightBoundary[stack.pop()] = i;
59 }
60
61 // Set left boundary for current element
62 leftBoundary[i] = stack.isEmpty() ? -1 : stack.peek();
63
64 // Push current index to stack
65 stack.push(i);
66 }
67
68 // Calculate maximum rectangle area for each bar as height
69 for (int i = 0; i < n; i++) {
70 // Width = rightBoundary - leftBoundary - 1
71 // Area = height * width
72 int width = rightBoundary[i] - leftBoundary[i] - 1;
73 int area = heights[i] * width;
74 maxArea = Math.max(maxArea, area);
75 }
76
77 return maxArea;
78 }
79}
80
1class Solution {
2public:
3 int maximalRectangle(vector<vector<char>>& matrix) {
4 int numCols = matrix[0].size();
5 vector<int> heights(numCols); // Heights array for histogram calculation
6 int maxArea = 0;
7
8 // Process each row to build histogram heights
9 for (auto& row : matrix) {
10 for (int col = 0; col < numCols; ++col) {
11 if (row[col] == '1') {
12 // Increment height if current cell is '1'
13 ++heights[col];
14 } else {
15 // Reset height to 0 if current cell is '0'
16 heights[col] = 0;
17 }
18 }
19 // Calculate max rectangle area for current histogram
20 maxArea = max(maxArea, largestRectangleArea(heights));
21 }
22
23 return maxArea;
24 }
25
26 int largestRectangleArea(vector<int>& heights) {
27 int maxArea = 0;
28 int n = heights.size();
29 stack<int> indexStack; // Stack to store indices of histogram bars
30
31 // Arrays to store the nearest smaller element indices
32 vector<int> leftBoundary(n, -1); // Index of nearest smaller element on left
33 vector<int> rightBoundary(n, n); // Index of nearest smaller element on right
34
35 // Single pass to find both left and right boundaries
36 for (int i = 0; i < n; ++i) {
37 // Pop elements from stack that are greater than or equal to current height
38 while (!indexStack.empty() && heights[indexStack.top()] >= heights[i]) {
39 // Current index i is the right boundary for the popped element
40 rightBoundary[indexStack.top()] = i;
41 indexStack.pop();
42 }
43
44 // If stack is not empty, top element is the left boundary for current element
45 if (!indexStack.empty()) {
46 leftBoundary[i] = indexStack.top();
47 }
48
49 indexStack.push(i);
50 }
51
52 // Calculate maximum area using the boundaries
53 for (int i = 0; i < n; ++i) {
54 // Width = rightBoundary - leftBoundary - 1
55 // Area = height * width
56 int width = rightBoundary[i] - leftBoundary[i] - 1;
57 int area = heights[i] * width;
58 maxArea = max(maxArea, area);
59 }
60
61 return maxArea;
62 }
63};
64
1function maximalRectangle(matrix: string[][]): number {
2 const numCols = matrix[0].length;
3 const heights: number[] = new Array(numCols).fill(0); // Heights array for histogram calculation
4 let maxArea = 0;
5
6 // Process each row to build histogram heights
7 for (const row of matrix) {
8 for (let col = 0; col < numCols; col++) {
9 if (row[col] === '1') {
10 // Increment height if current cell is '1'
11 heights[col]++;
12 } else {
13 // Reset height to 0 if current cell is '0'
14 heights[col] = 0;
15 }
16 }
17 // Calculate max rectangle area for current histogram
18 maxArea = Math.max(maxArea, largestRectangleArea(heights));
19 }
20
21 return maxArea;
22}
23
24function largestRectangleArea(heights: number[]): number {
25 let maxArea = 0;
26 const n = heights.length;
27 const indexStack: number[] = []; // Stack to store indices of histogram bars
28
29 // Arrays to store the nearest smaller element indices
30 const leftBoundary: number[] = new Array(n).fill(-1); // Index of nearest smaller element on left
31 const rightBoundary: number[] = new Array(n).fill(n); // Index of nearest smaller element on right
32
33 // Single pass to find both left and right boundaries
34 for (let i = 0; i < n; i++) {
35 // Pop elements from stack that are greater than or equal to current height
36 while (indexStack.length > 0 && heights[indexStack[indexStack.length - 1]] >= heights[i]) {
37 // Current index i is the right boundary for the popped element
38 const poppedIndex = indexStack.pop()!;
39 rightBoundary[poppedIndex] = i;
40 }
41
42 // If stack is not empty, top element is the left boundary for current element
43 if (indexStack.length > 0) {
44 leftBoundary[i] = indexStack[indexStack.length - 1];
45 }
46
47 indexStack.push(i);
48 }
49
50 // Calculate maximum area using the boundaries
51 for (let i = 0; i < n; i++) {
52 // Width = rightBoundary - leftBoundary - 1
53 // Area = height * width
54 const width = rightBoundary[i] - leftBoundary[i] - 1;
55 const area = heights[i] * width;
56 maxArea = Math.max(maxArea, area);
57 }
58
59 return maxArea;
60}
61
Time and Space Complexity
Time Complexity: O(m × n)
, where m
is the number of rows and n
is the number of columns in the matrix.
- The outer loop iterates through all
m
rows of the matrix - For each row, we update the heights array in
O(n)
time - For each row, we call
largestRectangleArea
which takesO(n)
time:- Building the
left
array:O(n)
- each element is pushed and popped from the stack at most once - Building the
right
array:O(n)
- each element is pushed and popped from the stack at most once - Finding the maximum area:
O(n)
- single pass through the heights
- Building the
- Total:
m × O(n) = O(m × n)
Space Complexity: O(n)
, where n
is the number of columns in the matrix.
heights
array:O(n)
space- In
largestRectangleArea
:left
array:O(n)
spaceright
array:O(n)
spacestk
(stack):O(n)
space in worst case
- All these are reused for each row, so the overall space complexity remains
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Histogram Height Calculation
A common mistake is not properly resetting the height when encountering a '0'. Some implementations might mistakenly keep the previous height or incorrectly calculate cumulative heights.
Incorrect Implementation:
# Wrong: This doesn't reset properly
for row in matrix:
for j, v in enumerate(row):
heights[j] = heights[j] + 1 if v == "1" else heights[j] # Doesn't reset!
Correct Implementation:
for row in matrix:
for j, v in enumerate(row):
heights[j] = heights[j] + 1 if v == "1" else 0 # Properly resets to 0
2. Off-by-One Error in Rectangle Width Calculation
When calculating the width of the rectangle using left and right boundaries, it's easy to make an off-by-one error.
Incorrect:
# Wrong: This includes the boundary positions width = right[i] - left[i]
Correct:
# The actual width excludes both boundaries width = right[i] - left[i] - 1
3. Stack Comparison Logic Error
When maintaining the monotonic stack, using the wrong comparison operator can lead to incorrect boundary detection.
Incorrect:
# Wrong: Using > instead of >= misses equal heights while stack and heights[stack[-1]] > height: stack.pop()
Correct:
# We need >= to handle bars of equal height correctly while stack and heights[stack[-1]] >= height: stack.pop()
4. Not Handling Edge Cases
Failing to handle empty matrices or single-element cases can cause runtime errors.
Problematic Code:
def maximalRectangle(self, matrix):
# Directly accessing matrix[0] without checking
heights = [0] * len(matrix[0]) # Crashes if matrix is empty
Robust Solution:
def maximalRectangle(self, matrix):
if not matrix or not matrix[0]:
return 0
heights = [0] * len(matrix[0])
5. Mixing Data Types
The input matrix contains strings ('0' and '1'), but calculations need integers. Forgetting to handle this type difference is a common error.
Incorrect:
# Wrong: Comparing with integer instead of string if matrix[i][j] == 1: # Should be "1" heights[j] += 1
Correct:
if matrix[i][j] == "1": # Correctly comparing with string heights[j] += 1
6. Inefficient Histogram Calculation
Creating a new heights array for each row instead of reusing and updating it leads to unnecessary memory allocation.
Inefficient:
for i in range(len(matrix)):
heights = [] # Creating new array each time
for j in range(len(matrix[0])):
# Calculate height from scratch for each row
h = 0
for k in range(i, -1, -1):
if matrix[k][j] == "1":
h += 1
else:
break
heights.append(h)
Efficient:
heights = [0] * len(matrix[0]) # Reuse the same array
for row in matrix:
for j, v in enumerate(row):
heights[j] = heights[j] + 1 if v == "1" else 0 # Update in-place
7. Incorrect Boundary Initialization
Setting wrong initial values for left and right boundaries can lead to incorrect area calculations.
Incorrect:
left = [0] * n # Wrong: should be -1 right = [n - 1] * n # Wrong: should be n
Correct:
left = [-1] * n # Indicates no smaller element on left right = [n] * n # Indicates no smaller element on right
These pitfalls highlight the importance of careful implementation details in this algorithm, particularly around boundary conditions, data type handling, and the monotonic stack logic.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!