⭐算法入门⭐《栈 - 单调栈》困难02 —— LeetCode 85. 最大矩形

🙉饭不食,水不饮,题必须刷🙉

C语言免费动漫教程,和我一起打卡!
🌞《光天化日学C语言》🌞

LeetCode 太难?先看简单题!
🧡《C语言入门100例》🧡

数据结构难?不存在的!
🌳《数据结构入门》🌳

LeetCode 太简单?算法学起来!
🌌《夜深人静写算法》🌌

一、题目

1、题目描述

  给定一个仅包含01、大小为rows x cols的二维二进制矩阵,找出只包含1的最大矩形,并返回其面积。
[ 0 1 0 0 0 0 1 1 1 0 1 1 1 0 1 1 ] \left[ \begin{matrix} 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 1\\ 1 & 0 & 1 & 1\\1 & 0 & 1 &1\end{matrix} \right] 0011100001110111
  样例输入: [ ["0", "1", "0", "0"], ["0", "0", "1", "1"], ["1", "0", "1", "1"], ["1", "0", "1", "1"] ]
  样例输出: 6 6 6

2、基础框架

  • c++ 版本给出的基础框架代码如下:
int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize){
}

3、原题链接

( 1 ) (1) (1) LeetCode 85. 最大矩形
( 2 ) (2) (2) 剑指 Offer II 040. 矩阵中最大的矩形

二、解题报告

1、思路分析

  • 考虑以矩阵的每行作为基准线,建立一个只有 1 直方图,如下图所示,表示的是以矩阵最后一行作为基准线,建立的直方图。
    、
  • 于是问题就转化成求直方图中的最大矩形了。

1. 朴素做法

  • 首先,提供一个最暴力的办法,然后再来逐渐优化。
  • 枚举一个左方柱 l l l 和一个右方柱 r r r ,然后用 RMQ 区间最值求出区间的高度最小值 h m i n ( l , r ) hmin(l, r) hmin(l,r),那么答案就是: m a x ( ( r − l + 1 ) ∗ h m i n ( l , r ) ) max( (r-l+1)*hmin(l,r) ) max((rl+1)hmin(l,r)) ( 1 ≤ l ≤ r ≤ n ) (1 \le l \le r \le n) (1lrn)
  • 这样做存在的问题就是时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的,而这个问题中 n n n 很大,我们需要想 O ( n ) O(n) O(n) 或者 O ( n l o g n ) O(nlogn) O(nlogn) 的方法。

2. 枚举左方柱

  • 延续之前的思路,我们还是枚举最左的那个方柱 l l l,那么它能够作为 “最左的那个方柱” 是有条件限制的,如图3所示:
    图3
  • 图中的方柱高度分别为 1,2,2,4,3 当枚举的 “最左的那个方柱” 为图中蓝色方柱(第四个方柱),那么以它为最左方柱的内接矩形的高度一定要大于它前一个方柱的高度,不然前一个方柱作为 “最左的那个方柱” ,得到的面积肯定会更大。
    在这里插入图片描述
    图4
  • 如上图所示,以第四个方柱作为左端点,向右扩展时, ( a ) 、 ( b ) (a)、(b) (a)(b) 合法, ( c ) (c) (c) 非法(因为它的左方柱可以继续往左延伸)。

3. 固定右方柱

  • 然后,我们来看这么一种情况,从左往右扫描,扫描到第 i i i 个方柱,前面的方柱都是单调不减的,并且 h ( i − 1 ) > h ( i ) h(i-1) > h(i) h(i1)>h(i),如图5所示,蓝色方柱就是第 i i i 个方柱。
    图5
  • 这时候我们找到了几个满足 h ( l ) > h ( i ) h(l) > h(i) h(l)>h(i) 的最左方柱 l l l,并且计算了几块候选面积,如图6所示:

图6

  • 上图中的三种情况都是合法的,并且我们并不知道它们哪个面积大(红色代表每种方案的内接矩形的面积),试想一下图 ( a ) (a) (a) 中第六个方柱的高度无限高,那么它的面积明显就会比 图 ( b ) (b) (b)、图 ( c ) (c) (c)中的大,反之则不一定;
  • 对于所有的 h ( l ) > h ( i ) h(l) > h(i) h(l)>h(i) 的这些情况我们都需要计算一次,而且只需要计算一次,因为他们能够到达的最右方柱就是 r = i − 1 r = i-1 r=i1,和第 i − 1 i-1 i1 个方柱以后的方柱已经完全没有关系了,并且我们可以把计算后的图形和图7进行等价:
    图7
  • 于是,我们发现扫描完第 i i i 个方柱以后,前面的形状又变成了一个单调不减的,所以这个过程就变成了不断得维护一个单调不减的序列,严格一点,应该说是单调递增,因为高度相同的情况,下标越小越优。
  • 这一步我们称为削峰。

4. 维护单调递增序列

  • 通过一个 单调栈 st 来维护一个单调递增序列,栈里存的是 每个位置的下标,高度在栈内从栈底到栈顶单调递增。
  • 从左往右扫描每个方柱,对于一个栈 s,令 栈顶元素为 s t . t o p ( ) st.top() st.top() 考虑两种情况:
  • 1)空栈 或者 栈顶元素的高度 小于 当前元素的高度,即 h [ s t . t o p ( ) ] < h [ i ] h[st.top()] < h[i] h[st.top()]<h[i] 时,入栈;
  • 2)否则,循环判断栈顶元素,如果 栈顶元素的高度 大于等于 当前元素的高度,即 h [ s t . t o p ( ) ] ≥ h [ i ] h[st.top()] \ge h[i] h[st.top()]h[i],出栈,并且更新最大面积 :
    a n s = m a x ( a n s , ( i − s t . t o p ( ) ) ∗ h [ s t . t o p ( ) ] ) ans = max(ans, (i - st.top()) * h[st.top()] ) ans=max(ans,(ist.top())h[st.top()])
  • 然后将最后出栈的那个元素继续入栈,并且将它的高度设置为 h [ i ] h[i] h[i] ,也就是上文提到的削峰。
  • 3)为了让所有元素都能出栈,我们需要在原高度最后面插入一个最小的数,即 -1。
  • 有关 的实现,可以参见以下文章:《画解数据结构》栈
  • 有关 单调栈 更多的内容,可以看以下这篇文章:夜深人静写算法(十一)- 单调栈

2、时间复杂度

  • 对于一个 n × m n \times m n×m 的矩阵,求直方图时,由于每个元素最多入栈一次,出栈一次。时间复杂度为 O ( m ) O(m) O(m),枚举每条基准线的时间复杂度为 O ( n ) O(n) O(n)
  • 所以总的时间复杂度为 O ( n m ) O(nm) O(nm)

3、代码详解

/************************************* 栈的顺序表实现 *************************************/

#define DataType int
#define maxn 10010

struct Stack {
    DataType data[maxn];     // (1)
    int top;
};

void StackClear(struct Stack* stk) {
    stk->top = 0;
}
void StackPushStack(struct Stack *stk, DataType dt) {
    stk->data[ stk->top++ ] = dt;
}
void StackPopStack(struct Stack* stk) {
    --stk->top;
}

DataType StackGetTop(struct Stack* stk) {
    return stk->data[ stk->top - 1 ];
}
int StackGetSize(struct Stack* stk) {
    return stk->top;
}
bool StackIsEmpty(struct Stack* stk) {
    return !StackGetSize(stk);
}

/************************************* 栈的顺序表实现 *************************************/

struct Stack stk;

int max(int a, int b) {
    return a > b ? a : b;
}

int largestRectangleArea(int* heights, int heightsSize){
    int i, maxRet = 0;
    int topIndex;
    struct Stack *st = &stk;
    StackClear(st);                                // (2)

    for (i = 0; i <= heightsSize; ++i) {           // (3)
        if ( i < heightsSize && ( StackIsEmpty(st) || heights[ StackGetTop(st) ] <= heights[i]) ) {
            StackPushStack(st, i);                 // (4)
        }
        else {
            while (!StackIsEmpty(st) && (i == heightsSize || heights[ StackGetTop(st) ] >= heights[i]) ) {                                    // (5)
                topIndex = StackGetTop(st);
                StackPopStack(st);
                maxRet = max(maxRet, (i - topIndex) * heights[topIndex]);
            }
            StackPushStack(st, topIndex);          // (6)
            if(i < heightsSize)
                heights[topIndex] = heights[i];    // (7)
        }
    }
    return maxRet;
}
int row[210], tmprow[210];

int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize){
    int i, j, col, ans, max = 0;
    memset(row, 0, sizeof(row));

    for(i = 0; i < matrixSize; ++i) {
        col = *matrixColSize;
        for(j = 0; j < col; ++j) {
            if ( matrix[i][j] == '1' ) {
                row[j] += 1;                         // (8)
            }else {
                row[j] = 0;
            }
        }
        for(j = 0; j < col; ++j) {
            tmprow[j] = row[j];                      // (9)
        }
        ans = largestRectangleArea(tmprow, col);
        if (ans > max) {
            max = ans;
        }
    }
    return max;
}
  • ( 1 ) (1) (1) 自行实现栈Stack
  • ( 2 ) (2) (2) 由于栈变量st是函数局部变量,所以可能涉及到上一组数据的缓存,所以要先清空;
  • ( 3 ) (3) (3) i == heightsSize是用来对当前栈中的数据进行出栈处理的;
  • ( 4 ) (4) (4) 维护一个从栈底到栈顶的单调递增栈;
  • ( 5 ) (5) (5) 如果 栈顶元素 大于等于 当前元素,则一直 弹出栈顶元素 进行比较;并且计算可行的矩阵;
  • ( 6 ) (6) (6)当前元素 的下标继续入栈;
  • ( 7 ) (7) (7) 削峰;
  • ( 8 ) (8) (8) 如果当前行为 1,则当前行的直方图高度应该等于上一行的直方图高度+1;否则,等于 0;
  • ( 9 ) (9) (9) 进行直方图生成的时候,由于在函数调用的时候会改变传参的值,所以需要将它拷贝一份出来。

三、本题小知识

   我们在考虑问题的时候,往往可以将一个问题和另一个问题联系起来,将另一个子问题抽成一个子函数,这样就可以屏蔽子函数的实现细节。


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值