面试题第三弹
题目:给定一个整形矩阵Map,其中的值只有0和1两种,求其中全是1的矩阵中,最大矩阵面积区域为1的数量。
例如:
1 1 1 0
其中,最大矩形区域有3个1,所以返回3.
再如
1 0 1 1
1 1 1 1
1 1 1 0
其中,最大的矩形区域有6个1,所以返回6
思路
首先我们来了解一个数据结构,与其说是一种数据结构倒不如说是一种工具。它叫单调栈(对此有所了解的同学可以跳过这段)
单调栈的功能:找出一列数中每个数离它最近的比它小的数(分为左右两个数),而使用一个栈作为辅助空间可以将暴力遍历的时间复杂度O(n^2)降到O(n).
举个栗子
一列数为: 1 2 9 7 3 5 4
栈:st(初始为空)
1.数组遍历到 1 ,st为空,直接压入栈,continue;
2.数组遍历到 2 ,st不为空, 且st.top()小于2,符合单调栈的要求,可以压入栈,continue;
3.数组遍历到 9 , st不为空, 且st.top()小于9,符合单调栈的要求,可以压入栈,continue;
4.数组遍历到 7 , st不为空,但是st.top()大于7,不符合单调栈的要求,应该弹出9且压入7,此时9左边第一个小于9的数即st.top()(弹出9之后的st的top), 9右边第一个小于9的数就是使它弹出来的这个数即7.
此时我们得到:
左边第一个小的数 | 该数 | 右边第一个小的数 |
---|---|---|
2 | 9 | 7 |
此时栈的结构为:
7 |
---|
2 |
1 |
5.数组遍历到3 , st不为空,但是st.top()大于3 , 不符合单调栈的要求,应该弹出7且压入3
此时我们得到:
左边第一个小的数 | 该数 | 右边第一个小的数 |
---|---|---|
2 | 9 | 7 |
2 | 7 | 3 |
此时栈的结构为:
3 |
---|
2 |
1 |
6.数组遍历到5 , st不为空,且st.top()小于5,符合单调栈的要求,可以压入栈,continue;
7.数组遍历到4 , st不为空,但是st.top()大于4,不符合单调栈的要求,应该弹出5且压入4
此时我们得到:
左边第一个小的数 | 该数 | 右边第一个小的数 |
---|---|---|
2 | 9 | 7 |
2 | 7 | 3 |
3 | 5 | 4 |
8.此时数组已经遍历完成,但st不为空,所以依次弹出栈中元素,此时栈的结构为:
4 |
---|
3 |
2 |
1 |
最终结果如图:
左边第一个小的数 | 该数 | 右边第一个小的数 |
---|---|---|
2 | 9 | 7 |
2 | 7 | 3 |
3 | 5 | 4 |
3 | 4 | None |
2 | 3 | None |
1 | 2 | None |
None | 1 | None |
以上简单地模拟了一下单调栈的工作原理
Q: 所以你跟我讲了半天的单调栈对题目有什么用吗?
A: 废话,当然有用处的,不然我讲它干嘛,用处嘛,你听我一步一步道来
我们这样考虑,以每一行为底,求以该行为底的每个数能向上连续的1的个数,什么意思呢?
举个栗子:
1 | 0 | 1 | 0 |
---|---|---|---|
0 | 1 | 1 | 1 |
0 | 1 | 1 | 1 |
这是一个矩阵,当以第0行为底时,该行每个数能向上连续1的结果如下:
1 0 1 0
当以第1行为底时,该行能向上连续1的结果如下:
0 1 2 1
当以第2行为底时,该行能向上连续1的结果如下:
0 2 3 2
然后我们把最后得到的 0 2 3 2 想象成一个直方图
o | |||
---|---|---|---|
o | o | o | |
o | o | o | |
0 | 2 | 3 | 2 |
(“o”表示该块可用)
显而易见可知,最大的合法矩形包含1的个数是6,是怎么看出来的呢,
比如从0开始,因为该列一个可用块都没有,所以0位置标记一个0;
然后到了2,该列有两个可用块,因为它左边为0比它小所以不能往左扩展,但可以往右扩展一直到右边界,所以它的矩形包含1的个数为2*3=6;
然后到了3,因为它左右两边的数就是离它最近的比它小的数,所以该列不能向左和向右扩展了,所以它的矩形包含1的个数为3*1=3;
然后到了2,因为它右边为边界,所以不能往右扩展了,但向左可以扩展到第一个2,所以它的矩形包含1的个数为2*3=6;
所以最大的矩形包含的1的个数为max{0,6,3,6} = 6
所以为题转化为求 0 2 3 2 这列数的每个数向左和向右的第一个比它小的数,这不就与单调栈联系上了吗。
代码区:
int maxRecFromBottom(vector<int> height)
{
if (height.empty())
{
return 0;
}
stack<int> st;
int maxArea = 0;
for (int i = 0; i < height.size(); i++)
{
while (!st.empty() && height[i] <= height[st.top()])
{
int j = st.top();
st.pop();
int k = st.empty() ? -1 : st.top();
int curArea = (i - k - 1) * height[j];
maxArea = max(maxArea, curArea);
}
st.push(i);
}
while (!st.empty())
{
int j = st.top();
st.pop();
int k = st.empty() ? -1 : st.top();
int curArea = (height.size() - k - 1) * height[j];
maxArea = max(maxArea, curArea);
}
return maxArea;
}
int maxRecSize(vector<vector<int>> map)
{
if (map.empty() || map.size() == 0 || map[0].size() == 0)
{
return 0;
}
int maxArea = 0;
vector<int> height(map[0].size(), 0); //构造map的列数个元素
for (int i = 0; i < map.size(); i++)
{
for (int j = 0; j < map[0].size(); j++)
{
height[j] = (map[i][j] == 0 ? 0 : height[j]+1);
int a = map[i][j];
int b = height[j];
}
maxArea = max(maxArea, maxRecFromBottom(height));
}
return maxArea;
}
观察代码我们发现,我们是改进了单调栈的用法的,
改进1:因为给定的数组中可能存在连续相同的数的情况,所以我们把弹出的条件改为stack.top()>=array[i],即相同的数不能往右扩展,只能往左扩展,这样也许不是太符合单调栈的条件,但这些相同的数中总会有一个可以代表他们的最大值,取该值即可。
改进2:我们压入栈的是数组的下标而不是数组的值,因为我们要计算面积,用下标方便计算且可以直接通过数组取到相应的值。