单调栈(详解)

单调栈详解及应用实例

今天学习了单调栈,特发此文,加深印象。


基础知识

单调栈的分类:

单调递增栈

单调递减栈


单调栈的应用:

求解下一个大于x元素或者是小于x元素的位置

单调栈的模板:

void next_Greater(int a[],int n)
{
    for(int i=n-1;i>=0;--i)
    {
        while(!st.empty() && st.top()<+a[i])
        {
            st.pop();
        }
        res[i]=st.empty()?-1:st.top();
        st.push(a[i]);
    }
}

思路

因为单调栈一般用于求解下一个大于x元素或者是小于x元素的位置,这里用求解大于x元素举例:

从后往前遍历

我们用一个栈来存放当前可能作为解的元素,及时pop掉没有用和不合法的元素,举个实际例子:

原数组为 1  3  4  5  2  9  6

现在需要返回一个数组,其中每个元素代表原数组中这个位置的元素之后第一个大于它的元素值

即:3  4  5  9  9  -1  -1

过程:

首先从后往前遍历,每个遍历的元素,栈中存放的元素是可能成为大于此元素的值,而一定不会成为大于这个元素的元素已经在之前被pop掉了,因为在遍历时,如果栈顶元素小于待判元素的话,说明当前的这个栈顶(待判元素后面的第一个可能解)不符合要求,这时就应该及时将栈顶pop掉,在之后的遍历中,每个元素后面的最大值就一定不可能是被pop掉的元素了,所以这时候栈中的元素从栈顶往下一直是单调递增的,而栈顶元素就是第一个大于待判元素的值。

更加形象的解释: 

可以类比成一座座山,较低的山会被较高的山挡住(在一条直线上看时),越往后只能看到越高的山,因为要求这个山的后面的山嘛,所以我们就可以从后面开始倒着遍历,将比较高的山存放到栈中,如果有山比栈顶的山还要高,那么从前往后看就会看到这个待判的山而不是当前栈顶的山,所以就将待判的山和栈中比较,直到待判的山成为最高的(栈底)或者栈中有更高的山,那么就将栈中更高的山记录下来,将待判的山入栈 

和单调队列相似:单调栈的核心也是把所有的元素都入栈,然后通过比较及时的pop掉没有意义的元素 

题目练习:

 1.题目一

解题思路:

单调栈 + 哈希表处理

我们可以先预处理 nums2,使查询 nums1中的每个元素在 nums2中对应位置的右边的第一个更大的元素值时不需要再遍历 nums2。于是,我们将题目分解为两个子问题:

第 1 个子问题:如何更高效地计算 nums2中每个元素右边的第一个更大的值;

第 2 个子问题:如何存储第 1 个子问题的结果。

我们可以使用单调栈来解决第 1 个子问题。倒序遍历 nums2,并用单调栈中维护当前位置右边的更大的元素列表,从栈底到栈顶的元素是单调递减的。

具体地,每次我们移动到数组中一个新的位置 i,就将当前单调栈中所有小于 nums2[i] 的元素弹出单调栈,当前位置右边的第一个更大的元素即为栈顶元素,如果栈为空则说明当前位置右边没有更大的元素。随后我们将位置 i 的元素入栈。

AC 代码如下:

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();
        vector<int> ans(m);
        unordered_map<int,int> mp;
        stack<int> st;
        //单调栈
        for(int i=nums2.size()-1;i>=0;--i)
        {
            int t = nums2[i];
            while(!st.empty() && t>=st.top()) st.pop();
            mp[t] = st.empty()?-1:st.top();
            st.push(t);
        }
        //因为题目规定了nums2是没有重复元素的,所以我们可以使用哈希表来解决第2个子问题,将元素值与其右边第一个更大的元素值的对应关系存入哈希表。
        for(int i=0;i<nums1.size();++i)
        {
            ans[i] = mp[nums1[i]];
        }
        //细节:因为在这道题中我们只需要用到nums2中元素的顺序而不需要用到下标,所以栈中可以直接存储nuns2中元素的值即可。
        return ans;
    }
};

2.题目二

思路和上面的板子题一样,只不过多了一个距离的计算,我们只需要用单调栈存下标即可。

代码如下:

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int back = temperatures.size();
        vector<int> ans(back);
        stack<int> st;//单调栈 存下标
        for(int i=back-1;i>=0;--i)
        {
            int t = temperatures[i];
            while(!st.empty() && temperatures[st.top()]<=t) st.pop();
            ans[i] = st.empty()? 0 : (st.top()-i);
            st.push(i);
        }
        return ans;
    }
};

3.题目三

这道题就是板子题反过来,从左往右遍历即可,前面的题是没找到一个更高的点,就把栈顶pop掉,这个是找到一个更高的,就把栈顶记录下来,作为低洼利用起来

class Solution {
public:
    int trap(vector<int>& height) {
        int ans=0;
        stack<int> st;
        int n = height.size();
        for(int i=0;i<n;i++)
        {
            int t = height[i];
            while(!st.empty() && t> height[st.top()])
            {
                int top = st.top();//此处是一个极小值点 可以作为低洼 
                st.pop();
                if(st.empty()) break;
                int l = st.top();//记录低洼的左端点
                int x = i - l - 1;
                int y = min(height[l],height[i]) - height[top];
                ans += x*y;
            }
            st.push(i);
        }
        return ans;
    }
};

当然也可以从右往左遍历,代码如下:

class Solution {
public:
    int trap(vector<int>& height) {
        int ans=0;
        stack<int> st;
        int n = height.size();
        for(int i=n-1;i>=0;i--)
        {
            int t = height[i];
            while(!st.empty() && t > height[st.top()])
            {
                int top = st.top();//此处是一个极小值点 可以作为低洼 
                st.pop();
                if(st.empty()) break;
                int l = st.top();//记录低洼的左端点
                int x = l - i - 1;
                int y = min(height[l],height[i]) - height[top];
                ans += x*y;
            }
            st.push(i);
        }
        return ans;
    }
};

4.题目四

这道题需要两次的单调栈,在最小值相等的情况下,区间长度越长,那么解就最优,所以我们不妨以让每一个数都作为一次最小值,然后通过两次单调递增栈来确定该值左右最近的第一个小于该值的元素,这样我们就能够确定了区间长度的范围,最终再遍历一遍即可跑出答案。

详解见代码:

// Problem: P12241 [蓝桥杯 2023 国 C] 最大区间
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P12241
// Memory Limit: 512 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define PII pair<int,int>
#define fi first
#define se second
const int N = 3e5+10;
int a[N],n,l[N],r[N],ans=0;
//数组L/R用于存放以每一个元素为最小值的时候满足条件(第一个小于该值的位置)的最大区间的左右两端
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	stack<int> st;//从左端找第一个小于该元素的位置
	for(int i=1;i<=n;i++)
	{
		while(!st.empty() && a[st.top()]>=a[i]) st.pop();//单调递增栈 因为只有比a[i]小的元素才会被留在栈中
		if(!st.empty()) l[i] = st.top();//单调递增栈 找第一个小于该值的元素
		else l[i] = 0;//说明当前的元素就是从左端开始到目前的最小值,记录下标为0
		st.push(i);
	}
	stack<int> s;//从右开始找第一个小于该元素的位置
	for(int i=n;i>0;i--)
	{
		while(!s.empty() && a[s.top()]>=a[i]) s.pop();
		if(!s.empty()) r[i] = s.top();
		else r[i] = n+1;//说明当前的元素就是从右端开始到目前为止的最小值,记录下标为n+1
		s.push(i);
	}
	for(int i=1;i<=n;i++) ans = max(ans,a[i]*(r[i]-l[i]-1));
	cout<<ans<<endl;
}

signed main()
{
	IOS
	int T=1;
//	cin>>T;
	while(T--) solve(); 
	return 0;
} 

5.好题

这道题目比较好 基本融合了两个模板~

// Problem: P1901 发射站
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1901
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define PII pair<int,int>
#define fi first
#define se second
const int N = 1e6+10;
int v[N],n,h[N],sum[N];

void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>h[i]>>v[i];
	}
	stack<int> st;
	for(int i=1;i<=n;i++)
	{
		while(!st.empty() && h[st.top()] < h[i])
		{
			sum[i] += v[st.top()];//模板一:对栈顶的利用
			st.pop();
		}
		if(!st.empty()) sum[st.top()] += v[i];//模板二:必要判空
		st.push(i);
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans = max(ans,sum[i]);
	cout<<ans<<endl;
}

signed main()
{
	IOS
	int T=1;
//	cin>>T;
	while(T--) solve(); 
	return 0;
} 

栈的相关练习

1.题目一

解题思路:子丑别嫌弃

class Solution {
public:
    int scoreOfParentheses(string s) {
        stack<int> st;
        //栈中的一个元素相当于是一个(),相邻的元素就是()()只需要相加即可
        st.push(0);//等价于先在字符串前面加一个()最后将这个值和字符串的值相加
        for (auto c : s)
        {
            if (c == '(')
            {
                st.push(0);
            }
            else//遇到一个右括号说明遇到了一个完整的级 先对这一级进行计算
            {
                //计算出这一级之后要与上一级融合
                int v = st.top();
                st.pop();
                st.top() += max(2 * v, 1);//因为有()和(A)  +=是()() 融合到同一级
            }
        }
        return st.top();
    }
};

2.题目二

字依然是一坨别嫌弃

class Solution {
public:
/*
栈内存放'('下标,保持栈底元素为 「最后一个没有被匹配的右括号的下标」
-1入栈,保持 栈底元素 为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」
遍历字符串,遇到'(',下标入栈
遇到')',出栈,表示匹配了当前右括号;若此时栈为空,说明该元素为 没有被匹配的右括号,
将其下标放入栈中,来更新「最后一个没有被匹配的右括号的下标」
*/
    int longestValidParentheses(string s) {
        stack<int> st;
        st.push(-1);//最后一个右括号的下标 作用是分割字符串,使之后的查找从此后开始 前面已经结束了 没有满足条件的括号了
        int result = 0;
        for(int i=0;i<s.length();i++){
            if(s[i] == '('){
                st.push(i);
            }
            else{
                st.pop();
                if(st.empty()){
                    st.push(i);//作用同上面的push(-1);
                }
                else{
                    result = max(result, i-st.top());
                }
            }
        }

        return result;
        
    }
};

小结

单调递栈:用于求第一个大于该值的元素 如果栈顶元素的值小于(等于)待判元素就弹出

最终只有大于待判元素的值能够留在栈中 因此栈从底到顶呈现递减趋势

单调递栈:用于求第一个小于该值的元素 如果栈顶元素的值大于(等于)待判元素就弹出

最终只有小于待判元素的值能够留在栈中 因此栈从底到顶呈现递增趋势

正向遍历:找出该值左边区域内的第一个大于/小于该值的元素

逆向遍历:找出该值右边区域内的第一个大于/小于该值的元素 

核心是在弹出操作结束后(形成了一个单调栈时)对栈顶元素(第一个大于/小于该值的元素)的利用 ,所以我们可以通过单调栈的核心思想及模版来找出该值最左/右端的第一个大于/小于该值的元素配合其他算法或者思路来进行求解

今天是集训的第二天,昨天学习了单调队列,对今天学习的单调栈有点帮助,但是感觉有点吃力,觉得单调栈和单调队列掌握的一般般,碰到新的题目还是不会写,甚至是看不出来用单调栈来写,特发此播客,养成善于总结的好习惯。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值