Given a rows x cols
binary matrix
filled with 0
's and 1
's, find the largest rectangle containing only 1
's and return its area.
Example 1
Input: matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] Output: 6 Explanation: The maximal rectangle is shown in the above picture.
Example 2
Input: matrix = [["0"]] Output: 0
Example 3
Input: matrix = [["1"]] Output: 1
Constraints:
rows == matrix.length
cols == matrix[i].length
1 <= row, cols <= 200
matrix[i][j]
is'0'
or'1'
.
- This one is indeed a hard problem and let me see how I can introduce the idea.
- First thing we can probably think of is the brute-force solution. However, it's not gonna be efficient. We all know why, because there're so many possibilities where the largest rectangle might locate.
- For example, assume the matrix dimension is
m * n
, we can start from any index of the matrix, that arem * n
possibilities. And for any of the indices, we can extend the largest rectangle's size from 1 to the rightmost columns and to the bottommost rows (assuming we only consider these directions so there are no duplicates). You can already see how inefficient this is. - And you'll probably also see that we might be duplicating the calculations if we are only blindly extending the matrix's dimensions, right? I mean, for example, when we start from row
i
and finished calculation for the largest rectangle with row size of1
, and when we try to find rectangles starting with rowi
and row size of2
, we would like to take advantage of the knowledge of thei
row we have already calculated, right? But, how? - I'm not sure how either, but let's get back to the problem itself. Let's see if we can find something useful.
- So we are trying to find the largest rectangle containing only
1
s, that means the largest rectangle cannot have0
s, That also means, if there's0
in the new row (or column) when we extend, at least for that row (or column), it won't form a rectangle of1
s. Well, duh! 🙄 - Lol, bear with me, we are definitely getting somewhere.
- So if we can somehow keep a record of how many
1
s for each column when we extend, we can "take" advantage of previous rows' information. For example, in the figure of Example 1, for the first three rows, you can use an array of1
s like[3,1,3,2,2]
to represent its current "state" (each number in the array means how many consecutive1
s ending at the bottom row). And trying to find a largest rectangle from the orignal matrix is same as finding it from this array. And it's fairly easy to keep a record of this array, right? (When you extend, if you meet a0
, it's reset to be0
; otherwise, add1
to its current value.) - So now the problem is converted to finding the largest rectangle from an array such as
[3,1,3,2,2]
. - We can definitely start from any of the array's index
i
and keep counting if we can form a rectangle with starting "height" at indexi
. But it's gonna beO(n2)
, withn
being the size of the array. Let's see if we can do better than that. - Now this is where the magic happens.
- You probably have already guessed, it's the "Stack", since it's the "Stack" series.
- Also, since I didn't think of this method myself, so I'll try to explain it directly, we can always revisit it and find out the logic behind it and try to apply it in other situations.
- In order to make it easy to understand the process, I also drew some illustrations like below, so let's see.
- Some context first, each bar in the figures is an element in the above-mentioned array. And we will use a stack to remember "some" indices of the visited indices. The bars with dashed border are the ones that's popped out from the stack.
- In Figure
I
, we visit the array at index0
, and we also enqueue its index0
to the stack. Because we don't know if the bars we'll see are shorter or higher, so we just "wait and see". - And also we'll keep enqueuing if we meet higher bars from the array. Why? Because the "largest" rectangle starting from the leftmost bar can continue extending, so we don't know if we should stop now.
- In Figure
II
, we enqueue two more bars since they're higher than previous ones. - In Figure
III
, we meet a bar at indexi
, that is shorter than previous one. Hmm, now we cannot keep extending, what do we do now? - We know one thing for sure that, if the largest rectangle starts from the bar before index
i
, it won't be able to extend to the right, because the bar at indexi
"failed" it. But, we can try to look backwards. - Also we know the bars we previously enqueued are in increasing order. That means the bars we enqueued can always extends until to the index before
i
. And we'll find out the largest one in backward order. - In Figure
III
, we pop the bar index at the stack top, and keep a record of its heightt
, and now the stack top points to the bar beforet
. So now we have a height, how do we find the width of the rectangle though? - Since we keep a record of indices in the stack, we know the index before index
t
. And we also know current indexi
. So the width would bei - (stack top) - 1
. Try to apply some numbers and you'll find out why we need to do- 1
. - As you can see from Figure
IV
, we know the width for our example is1
for the first rectangle. - OK, let's keep going. So we compare bar height at index
i
with the one at index at the stack top. We find out the bar at indexi
is shorter, so same logic, we pop the one at the stack top. (We know we cannot extend this one further passing indexi
, but we know we can extend until the index beforei
.) - Similarly, we found the bar with height
t
, and its width now is2
, as you can see in FigureV
. - In figure
VI
, as you can see, the bar at index0
with heightt
is still higher than the one at indexi
, we keep popping out the stack. We know the height ist
, but now the calculation for its width is a bit different. Because after popping out the stack, the stack becomes empty, so now the width becomes exactlyi
, why? - It's because array indices start from
0
, and the width before indexi
isi
. (You can still use our formula above, it's almost likei - (-1) - 1
, if you think of the stack top points to the index before0
, which is-1
.) - You might ask, what if the bar before
i
is shorter than the one ati
? Very good question, the answer is also simple. It's basically the same problem again. - Remember we are enqueuing the indices when we meet higher bars? So suppose the bar at index
0
is shorter than the bar at indexi
, we enqueue indexi
to the stack, and don't you find it similar? We already see the same problem in FigureII
. And because we record the indices onto the stack, we can always find the width correctly with our formular. And we don't care about missing some indices between0
andi
, because we know those bars are higher than bari
, and we can safely extend from index0
, until we find a shorter bar afteri
. - Careful readers might also ask, what if we meet a bar with same height as previous bar, the answer is we enqueue them, since higher bars and equal height bars function in the same way in this problem.
- Now the complexity decreases to
O(n)
, since we only enqueue and dequeue each index once. - I would say this is the magic of stack.
- Thanks for reading. Let me know if you still find it confusing.
const maximalRectangle = function(matrix) {
if (!matrix.length || !matrix[0].length) return 0;
const m = matrix.length, n = matrix[0].length;
const arr = Array(n).fill(0);
let max = 0;
for (let i = 0; i < m; i += 1) {
for (let j = 0; j < n; j += 1) {
arr[j] = matrix[i][j] == "1" ? arr[j] + 1 : 0;
}
max = Math.max(max, findMax(arr));
}
return max;
};
function findMax(heights) {
const stk = [];
let ans = 0, i = 0;
heights.push(0);
while (i < heights.length) {
if (!stk.length || heights[i] >= heights[stk[stk.length - 1]]) {
stk.push(i);
i += 1;
} else {
const t = stk.pop();
const width = !stk.length ? i : (i - stk[stk.length - 1] - 1);
ans = Math.max(ans, heights[t] * width);
}
}
return ans;
}