单调队列算法总结&专题训练

单调队列算法总结&专题训练

  • 一些 update
  • 1.概述
  • 2.模板
  • 3.例题
  • 4.总结

一些 update

update 2021/2/28:修改了『概述』部分。

1.概述

单调队列是一种特殊的队列,其保证队列内的元素单调递增。

而单调队列通常解决的是这样一类问题:

n n n 个数,给定长度 l e n len len,求所有区间 [ k , k + l e n ] ( k + l e n ≤ n , k ∈ [ 1 , n ] , k ∈ N ) [k,k+len](k+len \leq n,k \in [1,n],k \in N) [k,k+len](k+lenn,k[1,n],kN) 的最大最小值。

确实这个可以用 st 表,线段树之类的做,但是带一个 log。

而单调队列可以 O ( n ) O(n) O(n) 实现。

2.模板

以这道模板题为例,讲述单调队列的用法。link

手造一组样例(与例题所给略有不同):( n = 8 , k = 3 n=8,k=3 n=8,k=3

1 3 -1 -3 5 6 7 7

以求最小值为例,建立一个双端队列。

循环 i i i 从 1 到 n n n

i = 1 i=1 i=1 时,队列为空,插入 a 1 a_1 a1

队列: 1 ,最小值为 1 。

i = 2 i=2 i=2 时, a 2 = 3 a_2=3 a2=3,比 队首元素 要大,难道我们就要残忍抛弃 a 2 a_2 a2 吗?

不能! 虽然 a 2 > a 1 a_2 > a_1 a2>a1 ,但是随着时间的推移, a 1 a_1 a1首先被挤出队列,对后面不能做出贡献,但是 a 2 a_2 a2 活得更久 呆在队列里的时间更长,在 a 1 a_1 a1 离开之后就有可能成为答案,所以我们要将 a 2 a_2 a2 插入队列。(以上为单调队列一个重要思想)

队列: 1 3,最小值为 1 。

i = 3 i=3 i=3 时, a 3 = − 1 a_3=-1 a3=1,比前面的数都小,并且 a 3 a_3 a3呆在队列里的时间比 a 1 , a 2 a_1,a_2 a1,a2 更长,肯定比 a 1 , a 2 a_1,a_2 a1,a2 更优,那么要 a 1 , a 2 a_1,a_2 a1,a2有什么用?从队尾弹出 a 1 , a 2 a_1,a_2 a1,a2(这就是为什么要用双端队列而不是普通队列),插入 a 3 a_3 a3 。(以上为单调队列另一个重要思想)

所以发现了吗?我们其实是要在队列里面维护一个单调递增序列。

队列:-1,最小值为 -1。

i = 4 i=4 i=4 时, a 4 > 队 尾 元 素 a_4>队尾元素 a4> ,根据 i = 3 i=3 i=3 的推论,弹出 a 3 a_3 a3 ,插入 a 4 a_4 a4

队列:-3 ,最小值为 -3 。

i = 5 i=5 i=5 时, a 5 > 队 尾 元 素 a_5>队尾元素 a5> ,根据 i = 2 i=2 i=2 的推论,插入 a 5 a_5 a5

队列:-3 5

i = 6 i=6 i=6 时, a 6 > 队 尾 元 素 a_6>队尾元素 a6> ,根据 i = 2 i=2 i=2 的推论,插入 a 6 a_6 a6

队列:-3 5 6,最小值 -3。

i = 7 i=7 i=7 时, a 7 > 队 尾 元 素 a_7>队尾元素 a7> ,根据 i = 2 i=2 i=2 的推论,插入 a 7 a_7 a7。。。。。。吗?

要注意了!!!由于区间长度 k = 3 k=3 k=3 ,此时队首元素 a 4 = − 3 a_4=-3 a4=3 已经超出了区间长度的限制,无论有多小都不能对答案做出贡献,所以必须从队首弹出队列!

所以弹出 a 4 a_4 a4 ,插入 a 7 a_7 a7

队列:5 6 7,最小值为 5。

最后 i = 8 i=8 i=8 时,首先弹出 a 4 a_4 a4 (过时了),然后??? a 8 = a 7 = 7 a_8=a_7=7 a8=a7=7 ,那么需不需要更新呢?

需要!考虑到有区间长度的限制,所以 a 8 a_8 a8 必然比 a 7 a_7 a7 更优,因此需要从队尾弹出 a 7 a_7 a7 ,插入 a 8 a_8 a8

队列:6 7,最小值为 6,不过此时的 7 代表 a 8 a_8 a8

那么这样一来就有一个维护问题了,如何知道这个 7 代表 a 7 a_7 a7 还是 a 8 a_8 a8 呢?

实际写代码时为了避免这个情况,我个人通常会使用数组下标存到队列里面,这样就没有这个问题了,后面的代码都是如此。

理解如何使用单调队列求区间最小值后,求区间最大值就很轻松了,只需要仿照上述步骤,在队列内维护一个单调下降序列即可。

模板的代码:

#include
using namespace std;

const int MAXN=1e6+10;
int n,k,a[MAXN],ans[MAXN][2];
deque<int>q;

int read()
{
	int fh=1,sum=0;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		sum=(sum<<3)+(sum<<1)+ch-'0';
		ch=getchar();
	}
	return sum*fh;
}

void GetMin()
{
	deque<int>q;
	for(int i=1;i<=n;i++)
	{
		while(!q.empty()&&i-q.front()>=k) q.pop_front();
		while(!q.empty()&&a[i]<=a[q.back()]) q.pop_back();
		q.push_back(i);
		if(i-k+1>0) ans[i-k+1][0]=a[q.front()];
	}
}

void GetMax()
{
	deque<int>q;
	for(int i=1;i<=n;i++)
	{
		while(!q.empty()&&i-q.front()>=k) q.pop_front();
		while(!q.empty()&&a[i]>=a[q.back()]) q.pop_back();
		q.push_back(i);
		if(i-k+1>0) ans[i-k+1][1]=a[q.front()];
	}
}

int main()
{
	n=read();k=read();
	for(int i=1;i<=n;i++) a[i]=read();
	GetMin();
	GetMax();
	for(int i=1;i<=n-k+1;i++) cout<<ans[i][0]<<" ";
	cout<<"\n";
	for(int i=1;i<=n-k+1;i++) cout<<ans[i][1]<<" ";
	return 0;
}

如果你成功理解了上述代码,那么恭喜你,学会了单调队列!

接下来是几道例题。

3.例题

例题1:luogu P1714

暴力方法显而易见,那么我们如何使用单调队列解决此题呢?

推敲题意可以发现:假如我们取出一个 n ∗ n n*n nn 的矩阵 a a a ,那么这个矩阵的最大值一定是每一列最大值的最大值,最小值一定是每一列的最小值的最小值。

这就好做了!对每一列跑一次区间长度为 n n n 的单调队列,设 f [ i ] [ j ] [ 0 / 1 ] f[i][j][0/1] f[i][j][0/1] 表示第 j j j 列从第 i − k + 1 i-k+1 ik+1(如果小于 1 则从 1 开始) 行开始到第 i i i 行止的最大值/最小值,跑单调队列时存储答案,然后从第 n n n 行开始对每一行跑一次关于 f f f 的单调队列,计算最大值与最小值即可。更新答案时需要注意,只有 n ≤ j n \leq j nj 时才需要更新答案。

代码:

#include
using namespace std;

const int MAXN=1000+10;
int n,m,k,a[MAXN][MAXN],f[MAXN][MAXN][2],ans=0x7f7f7f7f;

int read()
{
	int fh=1,sum=0;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		sum=(sum<<3)+(sum<<1)+ch-'0';
		ch=getchar();
	}
	return sum*fh;
}

void lie()
{
	for(int j=1;j<=m;j++)
	{
		deque<int>qmax,qmin;
		for(int i=1;i<=n;i++)
		{
			while(!qmax.empty()&&i-qmax.front()>=k) qmax.pop_front();
			while(!qmin.empty()&&i-qmin.front()>=k) qmin.pop_front();
			while(!qmax.empty()&&a[i][j]>=a[qmax.back()][j]) qmax.pop_back();
			while(!qmin.empty()&&a[i][j]<=a[qmin.back()][j]) qmin.pop_back();
			qmax.push_back(i),qmin.push_back(i);
			f[i][j][0]=a[qmax.front()][j];
			f[i][j][1]=a[qmin.front()][j];
		}
	}
}//对每一列跑一次单调队列

void hang()
{
	for(int i=k;i<=n;i++)
	{
		deque<int>qmax,qmin;
		for(int j=1;j<=m;j++)
		{
			while(!qmax.empty()&&j-qmax.front()>=k) qmax.pop_front();
			while(!qmin.empty()&&j-qmin.front()>=k) qmin.pop_front();
			while(!qmax.empty()&&f[i][j][0]>=f[i][qmax.back()][0]) qmax.pop_back();
			while(!qmin.empty()&&f[i][j][1]<=f[i][qmin.back()][1]) qmin.pop_back();
			qmax.push_back(j),qmin.push_back(j);
			if(j>=k) ans=min(ans,f[i][qmax.front()][0]-f[i][qmin.front()][1]);
		}
	}
}//对每一行跑一次单调队列

int main()
{
	n=read();m=read();k=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			a[i][j]=read();
	lie();
	hang();
	cout<<ans<<"\n";
	return 0;
}

例题2:luogu P2698

详见这篇博客

例题3:烽火传递(Noip 2010 tg 初赛 完善程序 T2)

题目大意:给定数列 a 1... n a_{1...n} a1...n,从中选出若干个数使得连续 m m m 个数内至少有一个数被选中,求被选中的数和的最小值。

输入格式:

第一行 n , m n,m n,m ,第 2 行 n n n 个数表示 a 1... n a_{1...n} a1...n

范围: 1 ≤ m ≤ n ≤ 1 0 5 , 1 ≤ a i ≤ 100 1 \leq m \leq n \leq 10^5,1 \leq a_i \leq 100 1mn105,1ai100

输出格式:

求被选中的数和的最小值。

样例:

input:
5 3
1 2 5 6 2
output:
4

题解:

其实我并不是太懂出题人给的程序。。。所以怎么用单调队列呢?

这一道题有最小二字,结合数据范围,可以推断出这道题是一道 dp 题。(说贪心的请自行靠边站)

首先设状态:设 f i f_i fi 表示取第 i i i 个数后前 i i i 个数取数的最小值。

然后推方程: f i = min ⁡ ( f j ) + a i , j ∈ [ i − m , i − 1 ] f_i=\min(f_j)+a_i,j \in [i-m,i-1] fi=min(fj)+ai,j[im,i1] ,应该不难想吧。

所以如何使用单调队列呢?观察到 min ⁡ ( f j ) , j ∈ [ i − m , i − 1 ] \min(f_j),j \in [i-m,i-1] min(fj),j[im,i1],显然是固定长度 m m m ,可以使用单调队列维护。

这是一道经典的单调队列优化 dp 的题目,放代码:

#include
using namespace std;

const int MAXN=1e5+10;
int n,m,f[MAXN],a[MAXN],ans;
deque<int>q;

int read()
{
	int fh=1,sum=0;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		sum=(sum<<3)+(sum<<1)+ch-'0';
		ch=getchar();
	}
	return sum*fh;
}

int main()
{
	n=read();m=read();ans=0x7f7f7f7f;
	for(int i=1;i<=n;i++) a[i]=read();
	q.push_back(0);//记得把0推入队列,原因请自行思考
	for(int i=1;i<=n;i++)
	{
		while(!q.empty()&&i-q.front()>m) q.pop_front();
		f[i]=f[q.front()]+a[i];
		while(!q.empty()&&f[q.back()]>=f[i]) q.pop_back();
		q.push_back(i);
	}
	for(int i=n;i>=n-m+1;i--) ans=min(ans,f[i]);
	cout<<ans<<"\n";
	return 0;
}

例题4:CF372C

详见这篇博客

4.总结

相信在做完上述题目后,各位对单调队列有了一定程度的了解。单调队列可以用来维护一个序列上固定长度区间的最大最小值,可以与各种算法如 dp 相结合。

你可能感兴趣的:(数据结构,学习笔记,+,专项训练)