单调队列及练习

单调队列

单调队列是什么?简单点来说,就是维护一个队列,是这个队列中的元素由大到小或有小到大排列。每当加入一个新的元素时,如果队列尾部的元素不满足单调性的话,就将队尾元素弹出,直到满足单调性再将新元素插入

单调队列操作

1,删头:如果队首的元素脱离了窗口,他就没用了,弹出。

2,去尾:如果新元素入队时,原队尾的存在打破了单调性,就弹出。

例题1:滑动窗口(洛谷P1886)

有一个长为 n 的序列a,以及一个大小为 k 的窗口。

现在这个从左边开始向右滑动,每次滑动一个单位,

求出每次滑动后窗口中的最大值和最小值

这里就体现了单调队列第一个也是最重要的的一个用处,求区间内最值。我们用一个单调递增队列,和一个单调递减队列分别来求区间内的最小值和最大值。

#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int n , k , a[N];
deque<int> qz;//单调递增队列求最小值
deque<int> qj;//单调递减队列求最大值
int main()
{
	scanf("%d%d" , &n , &k);
	for(int i = 1 ; i <= n ; i++) scanf("%d" , &a[i]);
	for(int i = 1 ; i <= n ; i++)
	{
		while(!qz.empty() && a[qz.back()] > a[i]) qz.pop_back();//去尾
		qz.push_back(i);//记住是存下标!!!
		if(i >= k)
		{
			while(!qz.empty() && i - qz.front() + 1 > k) qz.pop_front();//删头
			printf("%d " , a[qz.front()]);
		}
	}
	cout << endl;
	for(int i = 1 ; i <= n ; i++)//同理
	{
		while(!qj.empty() && a[qj.back()] < a[i]) qj.pop_back();
		qj.push_back(i);
		if(i >= k)
		{
			while(!qj.empty() && i - qj.front() + 1 > k) qj.pop_front();
			printf("%d " , a[qj.front()]);
		}
	}
	return 0;
}

例题2:良好的感觉(洛谷P2422) 

kkk 做了一个人体感觉分析器。每一天,人都有一个感受值 A i
A i越大,表示人感觉越舒适。在一段时间 [i,j]内,人的舒适程度定义为:

[i,j] 中最不舒服的那一天的感受值 *  [i,j]中每一天感受值的和。

现在给出 kkk 在连续 N 天中的感受值,

请问,在哪一段时间,kkk 感觉最舒适?(1<=A<=100000)

 因为A为正数所以A尽量多,我们以每个节点为中心,向两边扩展,直到两边碰到比他小的就停止

预处理出前缀和,用每个点乘以这段区间的长度 取最大值即可

​
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
long long n , a[N] , sum[N];
int frt[N] , nxt[N];//frt[i]表示i前面第一个比他小的数的下标,nxt[i]表示i后面第一个比他小的数的下标
deque<int> q1;
deque<int> q2;
int main()
{
	scanf("%lld" , &n);
	for(int i = 1 ; i <= n ; i++)
	{
		scanf("%lld" , &a[i]);
		sum[i] = sum[i - 1] + a[i];//前缀和
	}
	for(int i = 1 ; i <= n ; i++)
	{
		while(!q1.empty() && a[q1.back()] > a[i])
		{
			nxt[q1.back()] = i - 1;//a[i]比a[q1.back()]小,所以nxt[q1.back()] = i - 1;
			q1.pop_back();
		}
		q1.push_back(i);
	}
	for(int i = 1 ; i <= n ; i++)
		if(nxt[i] == 0)
			nxt[i] = n;//如果a[i]后面没有比他小的,就直接到最后
	for(int i = n ; i >= 1 ; i--)//求前面需要倒着输入
	{
		while(!q2.empty() && a[q2.back()] > a[i])
		{
			frt[q2.back()] = i + 1;//a[i]比a[q2.back()]小,所以frt[q2.back()] = i + 1;
			q2.pop_back();
		}
		q2.push_back(i);
	}
	for(int i = 1 ; i <= n ; i++)
		if(frt[i] == 0)前面没有比他小的,就直接到开头
	long long maxn = 0;
	for(int i = 1 ; i <= n ; i++) maxn = max(maxn , (sum[nxt[i]] - sum[frt[i] - 1]) * a[i]);
	printf("%lld" , maxn);
	return 0;
}

​

习题:选数1(accoders 9750)

有N个数(N<=100000),在连续M(M<=N)个数里至少要有一个数被选择。 
求选出来数的最小总和 

思路都在代码中,可能略微简洁,请读者自行理解,如有困惑可以私信问我。

/*思路
单调队列优化dp
Dp[i]表示前i个数都满足条件的最小值
Dp[i] = dp[j] + a[i] (i - m <= j <= i - 1)
用单调队列找dp[i-m] 到 dp[i-1] 的最小值 */
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n , m , a[N] , dp[N];
deque<int> q;
int main()
{
    scanf("%d%d" , &n , &m);
    for(int i = 1 ; i <= n ; i++) scanf("%d" , &a[i]);
    q.push_back(0);
    for(int i = 1 ; i <= n ; i++)
    {
        while(!q.empty() && i - q.front() > m) q.pop_front();
        dp[i] = dp[q.front()] + a[i];
        while(!q.empty() && dp[q.back()] > dp[i]) q.pop_back();
        q.push_back(i);
    } 
    int ans = 2147483647;
    for(int i = n - m + 1 ; i <= n ; i++) ans = min(ans , dp[i]);
    printf("%d" , ans);
    return 0;
}

 温馨提示:本人经常在洛谷,CF,accoders上做题,大家可以注册一下,一起努力吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值