一、问题引入
有一个数列为:8, 7, 12, 5, 4, 2, 16, 9, 17,8,每次从左到右选取k个数,输出每个区间中的最大值。
这可以看做是一个窗口从左边移到右边的过程。
暴力方法自然是枚举一个右端点,然后通过for循环一个个比较,算出k个中的最大值。如果数据量较大,显然会超时。
因此,需要一种更快速的求区间最值的办法,单调队列就是不错的选择。
二、算法分析
原本的暴力做法,之所以会造成超时,是因为对每一个值,都进行了重复的比较。就是在找当前的f(i)的时候,i的前面k-1个数其实在算f(i-1)的时候我们就比较过了。那么我们能不能保存上一次的结果呢?当然主要是i的前k-1个数中的最大值了。答案是可以,这就要用到单调递减队列。
当然,可以维护单调递减队列,自然也可以维护单调递增队列,道理是一样的,此处不做赘述。
三、算法实现
单调递减队列是这么一个队列,它的头元素一直是队列当中的最大值,而且队列中的值是按照递减的顺序排列的。我们可以从队列的末尾插入一个元素,可以从队列的两端删除元素。
1. 插入元素:为了保证队列的递减性,我们在插入元素v的时候,在队尾的元素大于v之前,不断地将队尾元素出队,这个时候我们才将v插入到队尾。
为什么要让队尾元素出队?
我们所要求的是区间的最大值,那么既然当前值大于等于队尾元素,而且下标还更大,那它一定比队尾元素更有可能是后面可能的答案。因此,队尾元素失去了意义,完全可以出队。
为什么要将目前的v保留呢?
虽然v可能不是目前的最值,但是有可能是后面窗口中的最值,因此应保留。
2.删除元素:由于我们只需要保存i的前k-1个元素中的最大值,下标小于 i-k+1的元素不在窗口里,所以当index[队首元素]<i-k+1时,将队首元素删除。
四、维护举例(摘自曾妞妞的论文)
对于上面问题中的数列:8, 7, 12, 5, 4, 2, 16, 9, 17,8,可以构造一个长度为3的单调递减队列。
i=1:插入8,队列为:(8) {i=1<3,不输出队首8}
i=2:插入7,队列为:(8,7) {i=2<3,不输出队首8}
i=3:插入12,队列为:(12,8,7){12大于7和8,依次删除队尾的7和8,并插入12到队列,且i=3了,故输出队首的12}
i=4:插入5,队列为:(12,5) {i≥3,输出队首12}
i=5:插入4,队列为:(12,5,4) {i≥3,输出队首12}
i=6:插入2,队列为:(12,5,4,2){i≥3,2插入后,因为队列中的元素多于了3个,队首的12没用了故被删除,输出此时的队首5,}
i=7:插入16,队列为:(16 5,4,2) {16大于队尾的2、4、5,依次删除2、4、5,并输出队首16}
i=8:插入9,队列为:(16,9) {i≥3,输出队首16}
i=9:插入17,队列为:(17 16,9) {i≥3,输出队首17}
i=10:插入8,队列为(17,8) {i≥3,输出队首17}
五、代码实现
#include
#include
using namespacestd;
const intMAXN=1e6+5;
intn,k,maxa[MAXN],a[MAXN];
struct big
{
int num,index;
}q[MAXN];
int main()
{
ios::sync_with_stdio(false);
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
int head,tail;
head=1,tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&q[tail].num<=a[i]) tail--;
tail++;
q[tail].num=a[i];
q[tail].index=i;
while(head<=tail&&q[head].index<(i-k+1)) head++;
if(i>=k) cout<
<<" ";
}
return 0;
}
六、复杂度分析
时间复杂度如何计算?入队,出队会不会耗掉很多时间?似乎不太确定。
换一种思路,从每个数被操作了多少次来计算。不难发现,每个数最多入队一次,出队一次,因此时间复杂度为O(N)级别的。
至于空间复杂度,队列长度至多为N,因此也为N。
因此,单调队列不失为一种效率高,空间小的数据结构,是优化时空复杂度的不错选择。
七、应用范围
单调队列可以应用于查询最值,可以维护具有单调性的序列,经常用于优化DP。它是一种常用、速度快。编程容易的数据结构。你,值得拥有。
八、习题应用
POJ 2823
SMOJ 诺诺的队列
SMOJ 松果
SMOJ 放假
SMOJ 伟大的航路
SMOJ 道路游戏
SMOJ 玫瑰华尔兹