学习笔记——单调队列与单调栈

单调队列

作用

维护区间最值(最大值,最小值)

操作

入队操作:队尾入队,将之前破坏单调性的元素移除

出队操作:如果队首元素超过区间范围,将队首元素移除

性质

元素性质:队首元素,永远维护区间最值(最大值,最小值)

均摊时间复杂度O(1):每个元素最多入队一次,出队一次,共2n次,均摊于每个元素2n/n=2

代码实现

#include<iostream>
#include<deque>
#include<vector>
using namespace std;
void output(vector<int>arr,int n)
{
	int len=0;
	for(int i=0;i<n;i++)
	len+=printf("%3d",i);
	printf("\n");
	for(int i=0;i<=len+1;i++)
	printf("-");
	printf("\n");
	for(int i=0;i<n;i++)
	printf("%3d",arr[i]);
	printf("\n");
	return ;
 } 
int main()
{
	int n,k;//n为数组长度,k为窗口长度
	cin>>n>>k;
	deque<int>q;
	vector<int>arr;
	for(int i=1,a;i<=n;i++)//初始化数组
	{
		cin>>a;
		arr.push_back(a);
	}
	output(arr,n);//输出数组
	for(int i=0;i<n;i++)
	{
		while(!q.empty()&&arr[q.back()]>arr[i])q.pop_back();
        //队列非空且队尾元素违反单调性则进行队尾出队
		q.push_back(i);//新元素入队,存储的是下标不是值
		if(i-q.front()==k)q.pop_front();//如果超过窗口则队首出队
		printf("[%d,%d]:%d\n",max(0,i-k+1),i,arr[q.front()]);
	}
	return 0;
}

单调栈

和单调队列相比扔掉了从头部出元素的操作

作用

维护最近大于或小于关系

谁把我弹出去,谁就是最近大于/小于我的元素

单调递增栈(维护最近小于关系)

单调递减栈(维护最近大于关系)

操作

入栈操作:入栈顶时将违反单调性的元素移除

代码实现

#include<iostream>
#include<stack>
#include<vector>
using namespace std;
void output(vector<int>arr,int n)
{
	int len=0;
	for(int i=0;i<n;i++)
	len+=printf("%3d",i);
	printf("\n");
	for(int i=0;i<=len+1;i++)
	printf("-");
	printf("\n");
	for(int i=0;i<n;i++)
	printf("%3d",arr[i]);
	printf("\n");
	return ;
 } 
int main()
{
	int n;//数组长度
	cin>>n;
	vector<int>arr;
	stack<int>s;
	arr.push_back(-1);//左端
	for(int i=0,a;i<n;i++)
	{
		cin>>a;
		arr.push_back(a);
	}
	arr.push_back(-1);//右端
	output(arr,arr.size());//输出数组
	vector<int>l(arr.size());//存放左侧的最近小于关系
	vector<int>r(arr.size());//存放右侧的最近小于关系
    //求右侧的最近小于关系,正序入栈
	for(int i=0;i<=arr.size()-1;i++)
	{
		while(!s.empty()&&arr[s.top()]>arr[i])
		{
			r[s.top()]=i;//若被弹出,则说明存在最近小于关系	
			s.pop();
		}
		s.push(i);
	}
    //求左侧的最近小于关系,逆序入栈
	for(int i=arr.size()-1;i>=0;i--)
	{
		while(!s.empty()&&arr[s.top()]>arr[i])
		{
	 		l[s.top()]=i;//若被弹出,则说明具有最近小于关系
			s.pop();
				
		}
		
		s.push(i);
	}
	for(int i=1;i<=n;i++)
	{
		printf("arr[%d]=%d,l[%d]=%d,r[%d]=%d\n",
		i,arr[i],i,arr[l[i]],i,arr[r[i]]);
	}
	return 0;
}

例题

1.滑动窗口

题目描述

​ 给出一个长度为 N 的数组,一个长为 K 的滑动窗口从最左移动到最右,每次窗口移动,如下图:

找出窗口在各个位置时的极大值和极小值。


输入

​ 第一行两个数 N,K。

​ 第二行有 N 个数,表示数组中的元素。

输出

​ 输出两行,第一行为窗口在各个位置时的极小值,第二行为窗口在各个位置时的极大值。


样例输入

8 3
1 3 -1 -3 5 3 6 7

样例输出

-1 -3 -3 -3 3 3
3 3 5 5 6 7

代码实现

#include<iostream>
#include<deque>
using namespace std;
#define MAXSIZE 300000
int arr[MAXSIZE+5];
int min_item[MAXSIZE+5]={0};
int max_item[MAXSIZE+5]={0};
int main()
{
	int n,k;
	cin>>n>>k;
	deque<int>q1,q2;//q1维护区间最小值,q2维护区间最大值
	for(int i=0;i<n;i++)
	cin>>arr[i];
	for(int i=0;i<n;i++)
	{
		while(!q1.empty()&&arr[q1.back()]>arr[i])
		{
			q1.pop_back();
		}
		q1.push_back(i);
		while(!q2.empty()&&arr[q2.back()]<arr[i])
		{
			q2.pop_back();
		}
		q2.push_back(i);
		if(i>=k-1)
		{
			if(i-k==q1.front())q1.pop_front();
			if(i-k==q2.front())q2.pop_front();
			min_item[i]=arr[q1.front()];
			max_item[i]=arr[q2.front()];
		}
	}
	for(int i=k-1;i<n;i++)
	{
		printf("%d",min_item[i]);
		if(i!=n-1)printf(" ");
	}
	cout<<endl;
	for(int i=k-1;i<n;i++)
	{
		printf("%d",max_item[i]);
		if(i!=n-1)printf(" ");
	}
	return 0;
}

2.最大子序和

题目描述

​ 输入一个长度为 n 的整数序列,从中找出一段不超过 M 的连续子序列,使得整个序列的和最大。

​ 例如 1,−3,5,1,−2,3:

​ 当 m=4 时,S=5+1−2+3=7;

​ 当 m=2 或 m=3 时,S=5+1=6。


输入

​ 第一行两个数 n,m​。

​ 第二行有 n 个数,要求在 n 个数找到最大子序和。

输出

​ 一个数,数出他们的最大子序和。


样例输入

6 4
1 -3 5 1 -2 3

样例输出

7

思路

使用前缀和数组,使用单调队列维护区间最小值,枚举窗口(长度为m)的末尾位置i,遍历得sum[i+1]-sum[q.front()]的最小值,时间复杂度为O(n)。 

代码实现

#include<iostream>
#include<deque>
#include<cinttypes>
using namespace std;
#define MAXSIZE 300000
int arr[MAXSIZE+5];
int sum[MAXSIZE+5];
int main()
{
	int n,m;
	cin>>n>>m;
	sum[0]=0;
	for(int i=1;i<=n;i++)
	{
		cin>>arr[i];
		sum[i]+=sum[i-1]+arr[i];
	}
	deque<int>q;
	int ans=INT32_MIN;
	for(int i=0;i<n;i++)
	{
		while(!q.empty()&&sum[q.back()]>sum[i])
		{
			q.pop_back();
		}
		q.push_back(i);
		if(i-m==q.front())q.pop_front();
		ans=max(ans,sum[i+1]-sum[q.front()]);
	}
	cout<<ans;
	return 0;
}

3.设计自动结算系统

请设计一个自助结账系统,该系统需要通过一个队列来模拟顾客通过购物车的结算过程,需要实现的功能有:

  • get_max():获取结算商品中的最高价格,如果队列为空,则返回 -1
  • add(value):将价格为 value 的商品加入待结算商品队列的尾部
  • remove():移除第一个待结算的商品价格,如果队列为空,则返回 -1

注意,为保证该系统运转高效性,以上函数的均摊时间复杂度均为 O(1)

示例 1:

输入: 
["Checkout","add","add","get_max","remove","get_max"]
[[],[4],[7],[],[],[]]

输出: [null,null,null,7,4,7]

示例 2:

输入: 
["Checkout","remove","get_max"]
[[],[],[]]

输出: [null,-1,-1]

代码实现 

class Checkout {
public:
    deque<int>dq;
    queue<int>q;
    Checkout() {}

    int get_max() {
        if(dq.empty())return -1;
        return dq.front();
    }
    
    void add(int value) {
        q.push(value);
        while(!dq.empty()&&dq.back()<value)dq.pop_back();
        dq.push_back(value);
        return ;
    }
    
    int remove() {
        if(q.empty())return -1;
        int a=q.front();
        if(dq.front()==a)dq.pop_front();
        return a;
    }
};

4.绝对值不超过限制的最长连续子数组

给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。

如果不存在满足条件的子数组,则返回 0 。

示例 1:

输入:nums = [8,2,4,7], limit = 4
输出:2 
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4. 
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4. 
因此,满足题意的最长子数组的长度为 2 。

示例 2:

输入:nums = [10,1,2,4,7,2], limit = 5
输出:4 
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。

示例 3:

输入:nums = [4,2,2,2,4,4,2,2], limit = 0
输出:3

思路

变长滑动窗口,使用单调队列去维护区间最大值和最小值,若最大最小值的差小于等于limit,则窗口尾部向后滑一个位置,若最大最小值的差大于limit,则窗口头部向后滑,一直滑到符合要求的位置,在这期间记录窗口的最大长度。

代码实现

class Solution {
public:
    int longestSubarray(vector<int>& nums, int limit) {
        deque<int>max_q,min_q;
        int n=nums.size();
        int a=0,b=0;
        int ans=0;
        for(int i=0;i<n;i++)
        {
           b=i;
            while(!min_q.empty()&&nums[min_q.back()]>nums[i])min_q.pop_back();
            min_q.push_back(i);
            while(!max_q.empty()&&nums[max_q.back()]<nums[i])max_q.pop_back();
            max_q.push_back(i);
            if(nums[max_q.front()]-nums[min_q.front()]<=limit)
            {
                ans=max(ans,b-a+1);
                continue;
            }
            else
            {
                while(nums[max_q.front()]-nums[min_q.front()]>limit)
                {
                    if(min_q.front()==a)min_q.pop_front();
                    if(max_q.front()==a)max_q.pop_front();
                    a++;
                }
            }
        }
        return ans;
    }
};

5.最大矩形面积

题目描述

​ 给定从左到右多个矩形,已知这此矩形的宽度都为 1,长度不完全相等。这些矩形相连排成一排,求在这些矩形包括的范围内能得到的面积最大的矩形,打印出该面积。所求矩形可以横跨多个矩形,但不能超出原有矩形所确定的范围。


输入

​ 输入共一行,第一个数表示矩形的个数 N。接下来 N 个数表示矩形的大小。(1≤N≤100000)

输出

​ 输出最大矩形面积。


样例输入

7
2 1 4 5 1 3 3

样例输出

8

思路

 分别枚举每个小矩形的高,使用单调栈维护每个小矩形的左右最近小于他的位置,最后在所有的arr[i]*(r[i]-l[i]-1)的值里面取一个最大的值。

代码实现 

#include<iostream>
#include<stack>
using namespace std;
#define MAXSIZE 100000
long long arr[MAXSIZE+5];
long long l[MAXSIZE+5],r[MAXSIZE+5];
int main()
{
	int n;
	cin>>n;
	arr[0]=0;
	for(int i=1;i<=n;i++)
	cin>>arr[i];
	arr[n+1]=0;
	stack<int>s;
	for(int i=0;i<=n+1;i++)
	{
		while(!s.empty()&&arr[s.top()]>arr[i])
		{
			r[s.top()]=i;
			s.pop();
		}
		s.push(i);
	}
		for(int i=n+1;i>=1;i--)
	{
		while(!s.empty()&&arr[s.top()]>arr[i])
		{
			l[s.top()]=i;
			s.pop();
		}
		s.push(i);
	}
	long long ans=0;
	for(int i=1;i<=n;i++)
	ans=max(ans,arr[i]*(r[i]-l[i]-1));
	cout<<ans;
	return 0;
}

6.接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

 思路

使用单调栈去维护最近小于关系,每次遇到高度对栈顶元素矮就加进去,一直到遇到高的为止。遇到高的之后要不断对比它矮的元素进行出栈,同时计算体积。体积的计算为(min(h1,h3)-h2)*(j-i-1),对每一部分的体积进行累加,最后求得结果。

代码实现

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int>s;
        int n=height.size();
        int sum=0;
        for(int i=0;i<n;i++)
        {
            while(!s.empty()&&height[s.top()]<height[i])
            {
                int a=s.top();
                s.pop();
                if(!s.empty())
                {
                    int b=s.top();
                    sum+=(min(height[i],height[b])-height[a])*(i-b-1);
                }
                else continue;
            }
            s.push(i);
        }
        return sum;
    }
};

7.和至少为K的最短子数组

给你一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1 。

子数组 是数组中 连续 的一部分。

示例 1:

输入:nums = [1], k = 1
输出:1

示例 2:

输入:nums = [1,2], k = 4
输出:-1

示例 3:

输入:nums = [2,-1,2], k = 3
输出:3

思路

使用前缀和数组,利用单调队列维护值递增序列,每次入栈时将将要入栈的值和队尾元素比较,小则删至序列单调,大则在入队前和q.front()的值做差,大于等于k则记录,并继续往后找,否则退出循环,不断对符合条件的区间长度进行记录,最后找到最短值。

 代码实现

class Solution {
public:
    int shortestSubarray(vector<int>& nums, int k) {
        deque<int>q;
        int n=nums.size();
        int ans=n+1;
        vector<long long>sums(n+1);
        sums[0]=0;
        for(int i=1;i<=n;i++)
        sums[i]=sums[i-1]+nums[i-1];
        for(int i=0;i<=n;i++)
        {
            while(!q.empty()&&sums[q.back()]>sums[i])
            {
                q.pop_back();
            }
            while(!q.empty()&&sums[i]-sums[q.front()]>=k)
            {
                ans=min(ans,i-q.front());
                q.pop_front();
            }
            q.push_back(i);
        }
        if(ans==n+1)return -1;
        return ans;
    }
};

8.双生序列

题目描述

​ u,v 两个序列趋势相同,当且仅当对于任意 l 和 r,均有 RMQ(u,l,r)=RMQ(v,l,r) (1≤l≤r≤n),

​ 其中 n 是序列长度,RMQ(u,l,r) 是 u 序列从 l 到 r 中的最小值(有可能有多个最小值)的最大下标。

​ 现有两个序列 A={a1,a2,a3,…,an},B={b1,b2,b3,…,bn} 两个序列

​ 请求出最大的 p,使得A‘={a1,a2,a3,…,ap} 与B‘={b1,b2,b3,…,bp} 趋势相同。


输入

​ 第一行输入一个整数 n(1≤n≤500000),代表 A、B 序列长度。

​ 接下来两行,每行 n 个正整数,分别代表两个序列相应位置的值。

​ 序列中数字大小均在 int32 范围内。

输出

​ 输出一个整数,代表满足题意的最大 p 值。


样例输入

5
3 1 5 2 4
5 2 4 3 1

样例输出

4

 思路

在前i-1个位置为趋势相同的前提下,如何保证第i个位置加进去之后继续保持趋势相同?——两个单调队列相同,在此情景下等价于长度相同,否则第i个位置不可取,最终长度为i-1。

代码实现

#include<iostream>
#include<deque>
using namespace std;
#define MAXSIZE 500000
int a[MAXSIZE+5],b[MAXSIZE+5];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	for(int i=1;i<=n;i++)
	cin>>b[i];
	deque<int>qa,qb;
	for(int i=1;i<=n;i++)
	{
		while(!qa.empty()&&a[qa.back()]>a[i])
		{
			qa.pop_back();
		}
		while(!qb.empty()&&b[qb.back()]>b[i])
		{
			qb.pop_back();
		}
		if(qa.size()!=qb.size())
		{
			cout<<i-1;
			return 0;
		}
		qa.push_back(i);
		qb.push_back(i);
	}
	cout<<n;
	return 0;
}

相关推荐

  1. 单调单调队列所学的一些问题

    2024-02-22 01:26:01       24 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-02-22 01:26:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-22 01:26:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-22 01:26:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-22 01:26:01       20 阅读

热门阅读

  1. 【开源软件的影响力有多大?】

    2024-02-22 01:26:01       30 阅读
  2. 让图片说话SadTalker

    2024-02-22 01:26:01       33 阅读
  3. 嵌入式学习day22 Linux

    2024-02-22 01:26:01       29 阅读
  4. Linux--shell编程中的for循环

    2024-02-22 01:26:01       35 阅读
  5. SQL 和 NoSQL 有什么区别?

    2024-02-22 01:26:01       24 阅读
  6. 【菜鸡常见网络问题汇总】之:ARP详解

    2024-02-22 01:26:01       37 阅读
  7. 运动重定向学习笔记

    2024-02-22 01:26:01       29 阅读
  8. 数据安全:证书和密钥对概念详解

    2024-02-22 01:26:01       31 阅读
  9. @Validated 统一参数检验

    2024-02-22 01:26:01       27 阅读
  10. 前端工程化

    2024-02-22 01:26:01       26 阅读
  11. SQL常用函数收藏

    2024-02-22 01:26:01       23 阅读
  12. 前端关于Vue跳转外部链接(百度为例)

    2024-02-22 01:26:01       31 阅读
  13. firewall防火墙配置实战

    2024-02-22 01:26:01       30 阅读