单调栈原理+练习

首先用一道题引出单调栈

码蹄集 (matiji.net)

首先画一个图演示山的情况:

最暴力的做法自然是O(n方)的双循环遍历,这么做的思想是求出当前山右侧有多少座比它小的山,遇见第一个高度大于等于它的就停止。

但是对于我们所求的答案数,可以换一个想法,即对于一座山,求出它左侧有多少座山可以看到它。

对于5;7可以看到它。

对于3;7,5可以看到它。

对于4;7,5可以看到它。

可以发现,即求左侧有多少比当前值大的山的个数,那么我们可以维护一个数据结构,使得每次加进来一个数,都保持递减的顺序,然后每次加入前,都计算它的size大小,进行累加。比如:

一开始7进入;

然后5准备进入,进入前检查是否进入后会破坏单调性,发现不会,那么将当前的size累加到sum里,sum当前为1。5进入,现在数据结构为7,5;

然后3准备进入,检查发现不会破坏单调性,sum累加当前size(2),sum现在为(3)。3进入,现在数据结构为7,5,3;

然后4准备进入,检查发现会破坏单调性,就把比4小的全部扔出,于是3出去了。这时候sum累加当前size(2),sum现在为(5),然后4进入,数据结构现在为7,5,4;

结果也符合手算结果。

发现此过程进和出都是在同一侧进行,那么最适合的数据结构自然是栈了。这样的思想就是单调栈。

以下是代码:

#include<iostream>
#include<stack>
#define int long long
using namespace std;
signed main() {
    int n, num, sum=0;//n为山脉个数;num为每次读入的山脉高度,sum为总和
    stack<int> stk;//栈,我们要使它单调
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> num;
        //下面的while循环即用来维护单调性,本题要严格单调递减
        //每次检查栈顶元素是否小于等于当前高度,如果是则弹出,直到为空或者栈顶高度大于当前高度
        while (!stk.empty() && stk.top() <= num)
            stk.pop();
        //总和累加,stk.size即左侧比当前山脉高的山脉,也即能看到当前山的个数
        sum += stk.size();
        //入栈,栈此时满足单调性
        stk.push(num);
    }
    cout << sum;
}

(因为数据范围的要求,得用long long)

下面还有几道练习题

码蹄集 (matiji.net)

这道题需要我们脑补出究竟是哪种情况可以使海报数变少。

最多的情况自然是墙的个数n,考虑下面的情况:

如果出现这种情况,即低-高-低,并且左侧和右侧低的墙高度相同,那么就可以减少一张海报了。

注意中间高的墙可以不止一道,只要是被两个同高且相对低的墙夹住,整体就可以减少一张海报。

同样,可以没有所谓的高墙,即两道相连的墙同高,那自然也可以减少一张海报数。

而如果是这种情况:(涂橙色的代表墙)

那么即使左右两侧高度相同,还是需要用三张海报,因为题目要求海报不能超出边界。

我们同样维护一个数据结构,如果新输入的墙高于当前墙,那么就可以加入;而如果小于当前墙,那么就需要检查是否存在低-高-低的情况了。低的墙的高度即取决于新输入的墙的高度,将数据结构里所有比低墙要高的墙都弹出;若遇到同高的墙,则代表海报数可以减1,同时也弹出在数据结构里的那个同高的墙;若遇到比我们选定的低墙还低的墙,就结束弹出操作,并把新输入的墙加入。

以下面的数据为例,数字代表墙的高度:

2,2,3,4,2;以下是数据结构内容的变化

  • 2(第一个2加入)
  • 2(读入第二个2,发现同高,把前一个弹出,同时记录可以减1个海报)
  • 2-3(读入3,直接加入)
  • 2-3-4(读入4,直接加入)
  • 2(读入最后一个2,发现比顶部的墙小,开始弹出,发现4,3,2都要弹出,并且记录可以减1个海报数)

最后发现可以减少两个海报,那么就需要5-2=3个海报,就可以覆盖上述例子。

最终我们发现,维护的数据结构最终依然是单调的,并且操作在同侧进行,所以依然是单调栈的做法。

代码如下,代码量是很少的,但是需要知道其中的思想

#include<bits/stdc++.h> 
using namespace std;
int n,ans,d,w;
stack<int> st;
int main( )
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>d>>w;
        //只要栈顶的墙的高度大于等于墙的高度,都需要弹出
        while(!st.empty()&&w<=st.top()){
            if(st.top()==w)//如果遇见同高的墙,记录可以减少一个海报数
                ans++;
            st.pop();//弹出
        }
        st.push(w);//此时栈里的墙都比当前墙矮(或者栈为空),此时可以加入,就不会破坏单调性
    }
    cout<<n-ans;//最终结果为n-ans
    return 0;
}

下一道题

码蹄集 (matiji.net)

以样例为例,这道题也可用单调栈完成

维护一个单调递增的栈,一旦新增系数不满足单调性,即代表它是第一个小于栈顶元素系数的值。

按步骤拆解:

一开始1,3入栈

之后读入-3,开始检查,发现-3小于栈顶元素,于是x三次方的系数就变成-3,然后弹出3;之后-3与栈顶1对比,发现-3小于栈顶元素,于是x四次方的系数就是-3,然后弹出1 。-3入栈。

6入栈

读入1,比6小,所以x的系数是1 。但是比下一个栈顶元素-3大,于是入栈,现在栈里有-3,1;

最后x的平方和x的0次方系数为0.

这里有个问题,就是如果栈里存放系数的话,经过弹栈入栈,就判断不了这个系数是属于哪个x次幂的了,所以我们栈里改为存放下标i,通过下标读入存放在数组的系数。

以下是代码:

#include<bits/stdc++.h> 
#define ll long long
const int MOD=99887765;
const int N=5e6+7;
using namespace std;
stack<int> st;
//a是原系数数组,b是新的系数数组
int n,x,a[N],b[N];
ll ans;
int main( )
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>x;
    for(int i=1;i<=n+1;i++){
        cin>>a[i];
        //如果读入的系数小于栈顶的系数(注意栈里存放的是下标,于是通过a[st.top()]来获得栈顶的系数)
        while(!st.empty()&&a[i]<a[st.top()]){
            b[st.top()]=a[i];//栈顶位置的x次幂的系数设置为当前读入的系数(是第一个小于原系数的值)
            //弹栈
            st.pop();
        }
        //注意如入栈的是下标
        st.push(i);
    }
    //其实最好是显式初始化一下b数组,把初始值都设为0,虽然不这样做默认初值也是0.这样没处理的下标的保存的新系数自动设为0
    //这里就是计算多项式了
    for(int i=1;i<=n+1;i++){
        ans=ans*x+b[i];
        ans=(ans%MOD+MOD)%MOD;
    }
    cout<<ans;
    return 0;
}

以上就是一些单调栈的简单示例了。

似乎可以总结出一个模板了:

1.循环读入新元素

2.用一个while循环,通过对比栈顶和新元素,来把不满足单调性的栈顶弹出

3.新元素压栈,保持单调性

至于题目有什么要求,就要在模板里进行一些其他操作了

  • 29
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调栈是一种常用的数据结构,用于解决一类特定的问题,其中最常见的问题是找到数组中每个元素的下一个更大或更小的元素。在Codeforces编程竞赛中,单调栈经常被用于解决一些与数组相关的问题。 下面是单调栈的一般思路: 1. 创建一个空栈。 2. 从左到右遍历数组元素。 3. 对于每个元素,将其与栈顶元素进行比较。 - 如果当前元素小于等于栈顶元素,则将当前元素入栈。 - 如果当前元素大于栈顶元素,则将栈顶元素弹出,并将当前元素入栈。 4. 重复步骤3,直到遍历完所有元素。 这样,最后剩下的栈中元素就是没有下一个更大或更小元素的元素。在使用单调栈求解具体问题时,我们可以根据需要进行一些特定的操作。 例如,如果要找到一个数组中每个元素的下一个更大的元素,可以使用单调递减栈。具体操作如下: 1. 创建一个空栈和一个空结果数组。 2. 从左到右遍历数组元素。 3. 对于每个元素,将其与栈顶元素进行比较。 - 如果当前元素小于等于栈顶元素,则将当前元素入栈。 - 如果当前元素大于栈顶元素,则将栈顶元素弹出,并将其在结果数组中的位置记录为当前元素的下一个更大元素的索引。 4. 将当前元素入栈。 5. 重复步骤3和4,直到遍历完所有元素。 6. 结果数组中没有下一个更大元素的位置,可以设置为-1。 以下是一个使用单调递减栈求解下一个更大元素问题的示例代码: ```cpp #include <iostream> #include <stack> #include <vector> std::vector<int> nextGreaterElement(std::vector<int>& nums) { int n = nums.size(); std::vector<int> result(n, -1); std::stack<int> stack; for (int i = 0; i < n; i++) { while (!stack.empty() && nums[i] > nums[stack.top()]) { result[stack.top()] = i; stack.pop(); } stack.push(i); } return result; } int main() { std::vector<int> nums = {1,3, 2, 4, 5, 1}; std::vector<int> result = nextGreaterElement(nums); for (int i = 0; i < result.size(); i++) { std::cout << "Next greater element for " << nums[i] << ": "; if (result[i] != -1) { std::cout << nums[result[i]]; } else { std::cout << "None"; } std::cout << std::endl; } return 0; } ``` 以上代码将输出: ``` Next greater element for 1: 3 Next greater element for 3: 4 Next greater element for 2: 4 Next greater element for 4: 5 Next greater element for 5: None Next greater element for 1: None ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值