[week5]最大矩形——单调栈(线性结构的应用)

题意

给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
在这里插入图片描述

Input

输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。

Output

对于每组测试数据输出一行一个整数表示答案。

输入样例

7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0

输出样例

8
4000

提示


分析

这道题目并不难,重点在于理解单调栈及其应用


  • 单调栈

顾名思义,单调栈就是存储元素满足单调性的栈结构。单调性包括单调递增、单调递减、非单调递减、非单调递增。

为了满足单调栈,需要控制元素入栈出栈操作。

🌰——若当前希望该栈满足自栈底到栈顶单调递增,则应该保证每一个刚入栈的元素一定大于当前栈顶(或当前栈空)。若当前待入栈元素小于(等于)栈顶元素时,为了保持栈内元素的单调性,只有将栈顶元素依次弹出,直到当前待入栈元素大于栈顶元素(或栈空)。

同理,若栈底到栈顶单调递减,只需要通过适当选择入栈或弹栈,使栈内元素永远满足单调性即可。

  • 单调栈的应用

单调栈听上去感觉原理很简单,但一下子想不到它的作用。其实它的作用也很简单。

单调栈是用来找到一个元素在序列中大于或小于它的所有元素。一般主要用于查找一个元素在无序序列中第一个出现的大于或小于它的元素。

这时候就应该能想到,在一个无序序列中,比目标元素大或小的元素可能出现在它的左边或是右边。

💡单调栈又如何能够找到分布在不同方向的第一个符合要求的元素呢?

单调栈能解决的问题关键就在这里。

当我们一般在遍历一组序列时,通常是从左往右依次进行遍历。而在遍历过程依次将元素放入单调栈时,就能通过弹栈和入栈的情况确定当前目标元素所在位置往右的序列中第一个符合要求的元素了。因此,当我们反过来,从右往左对该序列进行遍历时,就能找到目标元素所在位置往左的序列中第一个符合要求的元素了。

💡不同单调性的单调栈能帮助找到满足不同要求的元素

比如,自栈底到栈顶单调递增的单调栈:能找到每个元素第一个比它小的元素。因为如果后入栈的元素需要弹栈时,就证明当前被弹出的元素都第一次遇到了比它们小的元素了。


  • 题目分析

如果要找到最大的阴影面积,则表示我们要找到一个高和宽乘积最大的阴影。

那么对于每一个高度,如果计算出它的最大宽度,那么就能通过比较所有高度的最大面积来选择出最终的答案了。

而对任一高度,如果它左右延伸的过程中所遇到的直方块高度比它高或是和它一样高,那么就代表该高度的面积能够继续延伸,即宽度进一步增加;反之,当遇到高度比它小的直方块时,说明它所能占的阴影不能再增加。

因此,只要计算出每一个高度在左右方向上遇到的第一个比它们低的直方块之间的距离,就能得到每个高度所能延伸的最大宽度,接着就能得到每个高度所占的最大面积了。这些面积中最大的即为题目所求。

  • 代码实现

显然,这个题目所要进行的操作正是单调栈的应用。

通过单调栈进行向右和向左的扫描,记录每个高度左右第一次遇见直方块的位置。用每个高度乘以其记录的两个位置标号之差,就能依次计算出所有面积。最后选出最大面积即可。

在两次遍历中,若当前遍历高度小于栈顶元素,则循环弹出栈顶元素直到当前遍历高度大于等于栈顶元素。所有弹出的栈顶元素将当前遍历高度的位置进行记录。

当一次单向遍历结束后,若栈中仍剩余元素,说明该高度在序列中此方向上并没有比它小的高度,则应该将其标号记为当前序列个数的最大或最小。


  • 本次程序遇到的问题

这次遇到的问题不多,但是都有点傻👋不过很值得记录下来参考!

  1. 数据类型和溢出

由题目可知,高度的数据范围为 0 <= hi <= 1000000000。在写代码的过程中,一般都会习惯性首先使用int类型。int类型的最大值为2147483647。显然这个数据范围在int能表示的范围之内。

但是如果使用int类型存储高度,会得到WA的结果。

此时我们发现,面积是由高度乘以宽度。显然面积是会超过int表示范围的。于是我们将面积改为long long型。

可是你会再次绝望的发现,仍然WA。

那么问题究竟出在哪儿?

仍然是溢出。当两个int型数据进行计算,结果已经溢出,即代表此时机器得到的结果已经是错误的,就算是赋给了long long型,得到的仍然是错误答案。

💡因此,若计算结果可能溢出,则应该将操作数及结果的数据类型都增大,才能保证计算结果的正确性。

  1. 若存储元素需要在未遍历它的情况下使用它的位置序号时,该怎么办?

这个就是有点点蠢了,脑袋没转的过来【狗头】

在我的这份代码中,我专门用了一个栈空间来存储单调栈中每个元素的标号,并保证这些标号的操作与其对应元素在单调栈中的操作相同,以此保证同步性。

实际上,有个很简单的方法,就是直接在单调栈中存储这些元素的标号,当需要用到这些元素进行比较或其他操作时,直接通过这些下标在数组中调用即可。


总结

  1. 单调栈究竟是递增还是递减是根本记不清的【233】但是知道啥时候用啥就行!
  2. 数据类型这种问题比较小,一般如果没想起就会消耗大量时间在调试上,很有可能改错都找不到问题。因此需要在以后每次书写代码的时候养成单独注意题目数据范围和自己所用数据类型是否会发生错误的习惯⚠️

代码

//
//  main.cpp
//  lab-a
//
//

#include <iostream>
#include <vector>
#include <stack>
using namespace std;

vector<long long> height;             //依次记录所有高度
int l[1000000];            //记录每个高度对应宽度的左右端点
int r[1000000];
stack<long long> area;                //用来找到每个阴影区域的左右端点
stack<int> order;               //用来记录当前area栈中高度所对应在直方图中的顺序

int main()
{
    ios::sync_with_stdio(false);
    
    int n = 0;
    long long h = 0;                //数据范围太大!!!
    //只是面积改为longlong同样会wa
    //int*int已经爆了再赋值给longlong结果就是错的
//    先运算再赋值 运算的时候已经错了 所以光面积用ll没用
    
    cin>>n;
    
    while( n != 0 )
    {
        long long max = 0;
        
        for( int i = 0 ; i < n ; i++ )
        {
            cin>>h;
            height.push_back(h);
        }
        
//        cout<<"---------"<<endl;
        
        for( int i = 0 ; i < n ; i++ )          //记录以每个直方块为高的阴影宽度的右端点
        {
//            cout<<height[i]<<" ** "<<endl;
            
            
            //找到该直方块右边第一个比它小的直方块
            while( !area.empty() && height[i] < area.top() )
                //若当前栈为空且待入栈元素小于栈顶元素,则弹出栈顶元素,直到符合要求
            {
//                cout<<order.top()<<" %% "<<endl;
//                r.insert(r.begin() + order.top(), i);   //将当前小于栈顶元素的直方图序号记录到栈顶元素对应序号的数组中
                
                r[order.top()] = i;
                
//                cout<<area.top()<<" && "<<endl;
                
                area.pop();                 //将栈顶元素弹出
                order.pop();                //将栈顶元素对应序号弹出
            }
            area.push(height[i]);           //将待入栈元素入栈
            order.push(i);                  //将其对应序号入栈
        }
        
        while( !order.empty() )             //最后剩下未出栈的元素右端点都为直方块个数+1
        {
//            cout<<" $$ "<<endl;
            r[order.top()] = n;
            order.pop();
            area.pop();
        }
        
        
//        cout<<"==========="<<endl;
        
        for( int i = n - 1 ; i >= 0 ; i-- )          //记录以每个直方块为高的阴影宽度左端点
        {
//            cout<<height[i]<<" ** "<<endl;
            
            //找到该直方块左边第一个比它小的直方块
            while( !area.empty() && height[i] < area.top() )
                //若当前栈为空且待入栈元素小于栈顶元素,则弹出栈顶元素,直到符合要求
            {
                l[order.top()] = i;
//                cout<<" @@ "<<endl;
//                l.insert(l.begin() + order.top(), i);
                //将当前小于栈顶元素的直方图序号记录到栈顶元素对应序号的数组中
                
//                cout<<area.top()<<" && "<<endl;
                
                area.pop();                 //将栈顶元素弹出
                order.pop();                //将栈顶元素对应序号弹出
            }
            area.push(height[i]);           //将待入栈元素入栈
            order.push(i);                  //将其对应序号入栈
        }
        
        while( !order.empty() )             //最后剩下未出栈的元素左端点都为-1
           {
               l[order.top()] = -1;
               order.pop();
               area.pop();
           }
        
        for( int i = 0 ; i < n ; i++ )              //计算得到最大阴影面积
        {
            if( height[i]*(r[i] - l[i] - 1) > max )
                max = height[i]*(r[i] - l[i] - 1);
            
//            cout<<i<<" / "<<height[i]<<" / "<<l[i]<<" / "<<r[i]<<" / "<<height[i]*(r[i] - l[i] - 1)<<endl;
        }
        
        cout<<max<<endl;
        
        height.clear();
//        l.clear();
//        r.clear();
        
        cin>>n;
        
    }
    
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天翊藉君

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

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

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

打赏作者

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

抵扣说明:

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

余额充值