一、概念介绍
1、 双端队列
双端队列是一种线性表,是一种特殊的队列,遵守先进先出的原则。双端队列支持以下4种操作:
- (1) 从队首删除
- (2) 从队尾删除
- (3) 从队尾插入
- (4) 查询线性表中任意一元素的值
2、 单调队列
单调队列是一种特殊的双端队列,其内部元素具有单调性。最大队列与最小队列是两种比较常用的单调队列,其内部元素分别是严格单调递减(不是非递增)和严格单调递增(不是非递减)的。
单调队列的常用操作如下:
- (1) 插入:若新元素从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列。
- (2) 获取最优(最大、最小)值:访问队首元素
以下是一个单调递增队列的例子:
队列大小不能超过3,入队元素依次为3,2,8,4,5,7,6,4
3入队:(3)
3从队尾出队,2入队:(2)
8入队:(2,8)
8从队尾出队,4入队:(2,4)
5入队:(2,4,5)
2从队头出队,7入队:(4,5,7)
7从队尾出队,6入队:(4,5,6)
6从队尾出队,5从队尾出队,4从队尾出队,4入队:(4)
以上左端为队头,右端为队尾。
二、单调队列的应用
1、最大值的维护:
比如我们要维护一个区间为k的最大值的单调队列,由于新插入 的节点他的“生命力”肯定比原先已经在队列中的元素“活”的时间长,将插入元素不断与队尾元素比, 如果他大于队尾元素,那么tail--将队尾元素删掉,(因为目前插入的这个元素值(设为pos)更大,而且“活”的时间 长,有pos在,队尾元素的有“生”之年永远都没法为最大值,故而直接无视比pos小的队尾了)。直到对空位置或者 找到了一个比pos大的队尾。
2、poj 2823 Sliding Window
Description
An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves rightwards by one position. Following is an example:
The array is [1 3 -1 -3 5 3 6 7], and k is 3.
Window position | Minimum value | Maximum value |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
Your task is to determine the maximum and minimum values in the sliding window at each position.
Input
The input consists of two lines. The first line contains two integers n and k which are the lengths of the array and the sliding window. There are n integers in the second line.
Output
There are two lines in the output. The first line gives the minimum values in the window at each position, from left to right, respectively. The second line gives the maximum values.
Sample Input
8 3 1 3 -1 -3 5 3 6 7
Sample Output
-1 -3 -3 -3 3 3 3 3 5 5 6 7
思路
一、
这道题目有个非常重要的信息,即所有的区间都是等长且连续的,那么对于“相邻”两个区间(l,r)与(l+1,r+1)有些极优美的性质:al,al+1,al+2,…,ar-1,ar,ar+1,以最大值为例:我们注意到,在区间(l,r)中,Max(al,al+1,al+2,…,ar-1,ar)=max(al,max(al+1,al+2,…,ar-1,ar))
在区间(l+1,r+1)中,Max(al+1,al+2,…,ar-1,ar,ar+1)=max(max(al+1,al+2,…,ar-1,ar),ar+1)
两个式子中有相同的部分max(al+1,al+2,…,ar-1,ar),经验告诉我们,区间(l,r)中最大值落在(l+1,r)区间的概率很大。那么,在求(l+1,r+1)的最值时,我们完全没有必要再扫描一次。只有当上一次的最大值落在了al上时才需要重新扫描,这样,算法得到了极大的优化。
继续考虑这样一个问题,以最大值为例,对于任意l<=i<=j<=r,如果ai<aj,那么,在区间向右移动的过程中,最大值永远也不会落在ai上,因为ai比aj先失效,能用ai一定能用aj,此时我们便不再需要ai了。这个性质与单调队列性质重合了。在扫描这个数列的时候,我们维护一个单调递减的队列。当我们将区间从(l,r)移动到(l+1,r+1)时,将ar+1 插入单调队列,若队首元素不在(l+1,r+1)时,删除它。这样处理后的队首元素便是(l+1,r+1)区间中的最大值。
二、
看这个问题:An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves rightwards by one position.Your task is to determine the maximum and minimum values in the sliding window at each position.
也就是有一个数列a,要求你求数列b和c,b[i]是a[i]…a[i+w-1]中的最小值,c[i]是最大值。如果a是1,3,-1,-3,5,3,6,7,则b为-1,-3,-3,-3,3,3,c为3,3,5,5,6,7。
这个问题相当于一个数据流(数列a)在不断地到来,而数据是不断过期的,相当于我们只能保存有限的数据(sliding window中的数据,此题中就是窗口的宽度w),对于到来的查询(此题中查询是每时刻都有的),我们要返回当前滑动窗口中的最大值最小值。注意,元素是不断过期的。
解决这个问题可以使用一种叫做单调队列的数据结构,它维护这样一种队列:
- a)从队头到队尾,元素在我们所关注的指标下是递减的(严格递减,而不是非递增),比如查询如果每次问的是窗口内的最小值,那么队列中元素从左至右就应该递增,如果每次问的是窗口内的最大值,则应该递减,依此类推。这是为了保证每次查询只需要取队头元素。
- b)从队头到队尾,元素对应的时刻(此题中是该元素在数列a中的下标)是递增的,但不要求连续,这是为了保证最左面的元素总是最先过期,且每当有新元素来临的时候一定是插入队尾。
满足以上两点的队列就是单调队列,首先,只有第一个元素的序列一定是单调队列。
那么怎么维护这个单调队列呢?无非是处理插入和查询两个操作。
对于插入,由于性质b,因此来的新元素插入到队列的最后就能维持b)继续成立。但是为了维护a)的成立,即元素在我们关注的指标下递减,从队尾插入新元素的时候可能要删除队尾的一些元素,具体说来就是,找到第一个大于(在所关注指标下)新元素的元素,删除其后所有元素,并将新元素插于其后。因为所有被删除的元素都比新元素要小,而且比新元素要旧,因此在以后的任何查询中都不可能成为答案,所以可以放心删除。
对于查询,由于性质b,因此所有该时刻过期的元素一定都集中在队头,因此利用查询的时机删除队头所有过期的元素,在不含过期元素后,队头得元素就是查询的答案(性质a),将其返回即可。由于每个元素都进队出队一次,因此摊销复杂度为O(n)。
#include<stdio.h> #include<string.h> const int maxn = 1000005; int a[maxn],q[maxn]; int n,k; void Monotonequeue(int op) { memset(q,0,sizeof(q)); bool first = true; int head = 1,tail = 0; for (int i = 1; i <= n; i++) { while (head <= tail && ((!op && a[i] <= a[q[tail]]) || (op && a[i] >= a[q[tail]]))) tail--; q[++tail] = i; while (head <= tail && q[tail] - q[head] >= k) head++; if (i >= k) first?printf("%d",a[q[head]]):printf(" %d",a[q[head]]),first = false; } printf("\n"); } int main() { scanf("%d%d",&n,&k); for (int i = 1; i <= n; i++) { scanf("%d",&a[i]); } Monotonequeue(0); //维护单调递增队列,队头元素最小 Monotonequeue(1); //维护单调递减队列,队头元素最大 return 0; }
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; typedef __int64 LL; const int maxn = 1000005; struct node { int pos,val; } q[maxn]; int num[maxn],n,k; int Min[maxn]; int Max[maxn]; void getMin() { int head=1,tail=0; for(int i=1; i<k; i++) { //前k-1个数跟查询无关,直接放进来 while(head<=tail&&num[i]<q[tail].val) tail--; //插入的准备操作,要么队列为空,要么找到一个位置使得符合单调队列定义 tail++; q[tail].val=num[i]; q[tail].pos=i; //插入 } for(int i=k; i<=n; i++) { while(head<=tail&&num[i]<q[tail].val) tail--; tail++; q[tail].val=num[i]; q[tail].pos=i; while(i-q[head].pos>=k) head++; //删除操作,维护最大的区间长度 Min[i-k]=q[head].val; //答案记录 } } void getMax() { int head=1,tail=0; for(int i=1; i<k; i++) { while(head<=tail&&num[i]>q[tail].val) tail--; tail++; q[tail].val=num[i]; q[tail].pos=i; } for(int i=k; i<=n; i++) { while(head<=tail&&num[i]>q[tail].val) tail--; tail++; q[tail].val=num[i]; q[tail].pos=i; while(i-q[head].pos>=k) head++; Max[i-k]=q[head].val; } } int main() { while(scanf("%d%d",&n,&k)!=EOF) { for(int i=1; i<=n; i++) scanf("%d",&num[i]); getMin(); for(int i=0; i<=n-k; i++) printf("%d%c",Min[i],i==n-k?'\n':' '); getMax(); for(int i=0; i<=n-k; i++) printf("%d%c",Max[i],i==n-k?'\n':' '); } return 0; }