题目来源
题目描述
class Solution {
public:
int numSubmat(vector<vector<int>>& mat) {
}
};
题目解析
问题:
- 必须以第0行为底的矩阵,其内部全是1的有多少个
- 必须以第1行为底的矩阵,其内部全是1的有多少个
- 必须以第2行为底的矩阵,其内部全是1的有多少个
- …
- 然后将上面的全部加起来,就是答案
为什么要必须以某一行作为地基呢?这样就即不会算多,也不会算少,因为矩阵必须以某一行为地基,第1行为地基的矩阵和第2行为地基的矩阵是不同的。
举个例子:
- 必须以第0行为底的矩阵,其内部全是1的子矩阵有:
{a}
、{b}
、{a,b]
- 一共
3
个
- 必须以第1行为底的矩阵,其内部全是1的有多少个
- 先算当前行:
{c}
、{d}
、{c,d}
- 然后以当前行为地基往上增长:
{a,c}
、{b,d}
、{a,b,c,d}
- 一共有6个
- 先算当前行:
那,以某一行为底的矩阵,其内部全是1的有多少个,应该怎么求呢?
- 在求以某一行打底时,可以按照数组直方图的思路,将当前行连同上面各行的数据处理下列,形成一个直方图,然后拿着直方图数组,去求个数
- 拿数组直方图求答案时,【单调栈】:
- 统计每个位置index,左侧比自己小、离自己最近的元素位置 leftIndex;右侧比自己小、离自己最近的元素位置 rightIndex;
- 则,形成的数组直方图宽度L = rightIndex - leftIndex + 1
- 如果只考虑当前打底行,个数 = L + (L-1) + (L - 2) + … + 1 = (L * (L + 1))/2;
- 还需要考虑直方图的高度 h ,总个数 = h * ((L * (L + 1))/2);
- 但这样会重复,为了避免重复,这里的 h = [index] - max([leftIndex], [rightIndex]);
- 所以,计算公式 =
( (L * (L + 1))/2 ) * ( [index] - max([leftIndex], [rightIndex]) )
举个例子:假设有一个直方图:
[
5
,
1
,
4
,
2
,
1
,
1
]
[5,1,4,2,1,1]
[5,1,4,2,1,1]
维护一个栈,要求单调增长
然后遍历数组。
- 首先遍历到0—>5,此时栈为空,因此0–>5入栈
- 然后遍历到1—>1,1—>1不能直接入栈,否则破坏单调性。为了让1—>1入栈,需要0—>5先出栈,出栈的数就要结算答案
因此:
- 对于0—>5来说:
- 左边比0—>5小的:没有,也就是不能往左扩,leftIndex =-1
- 右边以0—>5小的:就是旁边1—>6,所以右边不能扩张了,rightIndex =1
- 因此:必须以当前行为底(宽度为L = 1),以5作为高的矩阵,只有1个: 以0开始,到0结束
- 因此:必须以当前行为底(宽度为L =1),以4作为高的矩阵,只有1个 : 以0开始,到0结束
- 因此:必须以当前行为底(宽度为L = 1),以3作为高的矩阵,只有1个 : 以0开始,到0结束
- 因此:必须以当前行为底(宽度为L = 1),以2作为高的矩阵,只有1个 : 以0开始,到0结束
- 算不算高度为1的矩阵呢?不算,等着右边的0—>1弹出时来算
- 综上,一个得到:width * h = 1 * 5个
0—>5结算完答案之后:
- 现在1—>1可以入栈了
- 然后遍历数组,到了2—>4,2—>4可入栈
- 然后遍历数组,到了3—>2,3—>2不可直接入栈,因为会破坏单调性。所以需要先将栈顶元素2—>4出栈,出栈的同时也将结算答案
因此:
- 对于2—>4来说:
- 左边比它小的数是1—>1
- 右边比它小的数是3—>2
- 所以只有自己[2开始,2结束]组成一个单位
- 因此: 以当前行为地基(宽度为1),高度为4的子矩阵有1个
- 因此: 以当前行为地基(宽度为1),高度为3的子矩阵有1个
- 对于:以当前行为地基(宽度为1),高度为2的子矩阵,不去算,就算现在不算,反正总有算的时候
2—>4结算完答案之后:
- 3—>2可以入栈了
- 然后继续遍历,遍历到了4—>2了,它和栈顶元素值相同,要不要入栈呢?
- 入栈,当前遍历的数无论如何也要入栈(因为每个数只遍历一次,如果不记录下来的话,信息会丢失)
- 既然要入栈,那么栈顶元素就必须3—>2出栈,一般来说,出栈意味着结算答案,但是3—>2发现将自己释放的元素是4—>2,我们之间的值相同,那么我就不结算答案了,因为3—>2和4—>2是联通的,可以等待4—>2结算答案时一起结算
现在4–>2入栈了,我们继续遍历
- 遍历到了5—>1,5—>1不能直接入栈,必须将4—>2出栈,此时4---->2要结算答案
- 对于4—>2来说:
- 左边比它小的数是:1---->1
- 右边比它小的数是:5---->1
- 也就是对于当前行,当前高来说,左边扩张的边界是1,右边扩张的边界是5,所以[2,4]组成的区间都是可选的区域。
- 因此:必须以当前行为底(宽度为L = 3),以2为高度的矩阵有:
- 以2开始,到2结束,组成一个矩阵
- 以2开始,到3结束,组成一个矩阵
- 以2开始,到4结束,组成一个矩阵,共L = 3个
- 以3开始,到3结束,组成一个矩阵
- 以3开始,到4结束,组成一个矩阵,共L - 1 = 2个
- 以4开始,到1结束,组成一个矩阵,共L - 2 = 1个
- 因此:必须以当前行为底(宽度为L = 3),以1为高度的矩阵有:不去算,因为它左/右边比它小的数就是1
4—>2结算完答案之后,5—1入栈,此时栈中元素为
现在数组已经遍历完了,但是栈不为空,我们要把栈倒空
- 对于5---->1
- 对于1----1
class Solution {
// 计算以某一行打底(形成的直方图数组height)的全1子矩阵的数量
int process(std::vector<int> & height){
int m = height.size();
int count = 0;
std::stack<int> stack; // 单调栈:栈底 -> 栈顶: 由小 -> 大
// 每个元素依次入栈,维持单调栈的严格单调递增结构,不符合时,弹出元素,弹出即结算
for (int i = 0; i < m; ++i) {
// 维持单调栈的严格单调递增结构,不符合时,弹出元素,弹出即结算
while (!stack.empty() && height[i] <= height[stack.top()]){
int index = stack.top(); stack.pop();
int leftIndex = stack.empty() ? -1 : stack.top();// 左侧比[index]小、离index最近的元素位置
int rightIndex = i; // 右侧比[index]小、离index最近的元素位置
int wid = rightIndex - leftIndex - 1; // 形成的直方图长度
int hei = height[index] - std::max(
leftIndex == -1 ? 0 : height[leftIndex], height[rightIndex]
);
count += ((wid * (wid+1))/2) * hei; // 公式计算个数
}
stack.push(i);
}
// 结算单调栈中最后剩下的元素,弹出元素,弹出即结算
while (!stack.empty()) {
int index = stack.top(); stack.pop();
int leftIndex = stack.empty() ? -1 : stack.top();// 左侧比[index]小、离index最近的元素位置
int rightIndex = m; // 右侧比[index]小、离index最近的元素位置
int l = rightIndex - leftIndex - 1; // 形成的直方图长度
// 形成的直方图的有效结算高度 h :
int h = height[index] - (leftIndex == -1 ? 0 : height[leftIndex]);
count += ((l * (l+1))/2) * h; // 公式计算个数
}
return count;
}
public:
int numSubmat(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
int ans = 0;
std::vector<int> height(m); // 以每一行打底的直方图数组
for (int i = 0; i < m; ++i) {
// 计算以每一行打底的直方图
for (int j = 0; j < n; ++j) {
height[j] = mat[i][j] == 0 ? 0 : height[j] + 1;
}
// 计算以每一行打底时的全1子矩阵的数量
int cnt = process(height);
ans += cnt;
}
return ans;
}
};
优化方法是使用数组来实现栈