单调队列及其应用(双端队列)
单调队列,望文生义,就是指队列中的元素是单调的。如:{a1,a2,a3,a4……an}满足a1<=a2<=a3……<=an,a序列便是单调递增序列。同理递减队列也是存在的。
单调队列的出现可以简化问题,队首元素便是最大(小)值,这样,选取最大(小)值的复杂度便为o(1),由于队列的性质,每个元素入队一次,出队一次,维护队列的复杂度均摊下来便是o(1)。
如何维护单调队列呢,以单调递增序列为例:
1、如果队列的长度一定,先判断队首元素是否在规定范围内,如果超范围则增长队首。
2、每次加入元素时和队尾比较,如果当前元素小于队尾且队列非空,则减小尾指针,队尾元素依次出队,直到满足队列的调性为止
要特别注意头指针和尾指针的应用。
直观的感觉:单调队列存储了部分范围内的最大元素
这样一个队列,可以从两头删除,只能从队尾插入。单调队列的具体作用在于,由于保持队列中的元素满足单调性,对于元素便是极小值(极大值)了。
应用:
1.确定区间长度的范围最值:
给定一个长度为N的整数数列a(i),i=0,1,...,N-1和区间长度k.
要求:
f(i) = max{a(i-k+1),a(i-k+2),..., a(i)},i = 0,1,...,N-1
问题的另一种描述就是用一个长度为k的窗在整数数列上移动,求窗里面所包含的数的最大值。
解法一:
很直观的一种解法,那就是从数列的开头,将窗放上去,然后找到这最开始的k个数的最大值,然后窗最后移一个单元,继续找到k个数中的最大值。
这种方法每求一个f(i),都要进行k-1次的比较,复杂度为O(N*k)。
那么有没有更快一点的算法呢?
解法二:
我们知道,上一种算法有一个地方是重复比较了,就是在找当前的f(i)的时候,i的前面k-1个数其它在算f(i-1)的时候我们就比较过了。那么我们能不能保存上一次的结果呢?当然主要是i的前k-1个数中的最大值了。答案是可以,这就要用到单调递减队列。
单调递减队列是这么一个队列,它的头元素一直是队列当中的最大值,而且队列中的值是按照递减的顺序排列的。我们可以从队列的末尾插入一个元素,可以从队列的两端删除元素。
1.首先看插入元素:为了保证队列的递减性,我们在插入元素v的时候,要将队尾的元素和v比较,如果队尾的元素不大于v,则删除队尾的元素,然后继续将新的队尾的元素与v比较,直到队尾的元素大于v,这个时候我们才将v插入到队尾。
2.队尾的删除刚刚已经说了,那么队首的元素什么时候删除呢?由于我们只需要保存i的前k-1个元素中的最大值,所以当队首的元素的索引或下标小于 i-k+1的时候,就说明队首的元素对于求f(i)已经没有意义了,因为它已经不在窗里面了。所以当index[队首元素]<i-k+1时,将队首 元素删除。
从上面的介绍当中,我们知道,单调队列与队列唯一的不同就在于它不仅要保存元素的值,而且要保存元素的索引(当然在实际应用中我们可以只需要保存索引,而通过索引间接找到当前索引的值)。
为了让读者更明白一点,我举个简单的例子。
假设数列为:8,7,12,5,16,9,17,2,4,6.N=10,k=3.
那么我们构造一个长度为3的单调递减队列:
首先,那8和它的索引0放入队列中,我们用(8,0)表示,每一步插入元素时队列中的元素如下:
0:插入8,队列为:(8,0)
1:插入7,队列为:(8,0),(7,1)
2:插入12,队列为:(12,2)
3:插入5,队列为:(12,2),(5,3)
4:插入16,队列为:(16,4)
5:插入9,队列为:(16,4),(9,5)
。。。。依此类推
那么f(i)就是第i步时队列当中的首元素:8,8,12,12,16,16,。。。
注意单调队列的复杂度是O(1),因为对于每个元素的入队出队均摊时间都是O(1)
单调队列适用于固定区间最值:sliding windows
解决这个问题可以使用一种叫做单调队列的数据结构,它维护这样一种队列:
a)从队头到队尾,元素在我们所关注的指标下是递减的(严格递减,而不是非递增),比如查询如果每次问的是窗口内的最小值,那么队列中元素从左至右就应该递增,如果每次问的是窗口内的最大值,则应该递减,依此类推。这是为了保证每次查询只需要取队头元素。
b)从队头到队尾,元素对应的时刻(此题中是该元素在数列a中的下标)是递增的,但不要求连续,这是为了保证最左面的元素总是最先过期,且每当有新元素来临的时候一定是插入队尾。
poj2823
#include <iostream> #include <cstdio> using namespace std; const int MAX = 1000001; //两个单调队列 int dq1[MAX]; //一个存单调递增 int dq2[MAX]; //一个存单调递减 int a[MAX]; int main(void) { int i,n,k,front1,front2,tail1,tail2,start,ans; while(scanf("%d %d",&n,&k)!=EOF) { for(i = 0 ; i < n ; ++i) cin>>a[i]; front1 = 0, tail1 = -1; front2 = 0, tail2 = -1; ans = start = 0; for(i = 0 ; i < k ; ++i) { //front <=tail 非空 while(front1 <= tail1 && a[ dq1[tail1] ] <= a[i]) //当前元素大于单调递增队列的队尾元素的时候,队尾的元素依次弹出队列,直到队尾元素大于当前当前元素的时候,将当前元素插入队尾 --tail1; dq1[ ++tail1 ] = i; //只需要记录下标即可,值可以用数组自然得到 while(front2 <= tail2 && a[ dq2[tail2] ] >= a[i]) //当前元素小于单调递减队列的队尾元素的时候,队尾的元素依次弹出队列,直到队尾元素小于当前当前元素的时候,将当前元素插入队尾 --tail2; dq2[ ++tail2 ] = i; //只需要记录下标即可 } //从a[0]到a[k-1]的预处理 for( ; ; ++i) { printf("%d ",a[ dq2[ front2 ] ]); if(i==n) break; while(front2 <= tail2 && a[ dq2[tail2] ] >= a[i]) --tail2; dq2[ ++tail2 ] = i; while(dq2[ front2 ] <= i - k) ++front2; } for(i=k ; ; ++i) { printf("%d ",a[ dq2[ front2 ] ]); if(i==n) break; while(front1 <= tail1 && a[ dq1[tail1] ] <= a[i]) --tail1; dq1[ ++tail1 ] = i; while(dq1[ front1 ] <= i - k) ++front1; } } return 0; }
hdu 3415
1.如何处理序列和
2.sum[i]要注意
3.如何处理环
题目大意:给出一个有N个数字(-1000..1000,N<=10^5)的环状序列,让你求一个和最大的连续子序列。这个连续子序列的长度小于等于K。
分析:因为序列是环状的,所以可以在序列后面复制一段(或者复制前k个数字)。环的处理手法!!如果用s[i]来表示复制过后的序列的前i个数的和,那么任意一个子序列[i..j]的和就等于s[j]-s[i-1]。对于每一个j,用s[j]减去最小的一个s[i](i>=j-k+1)就可以得到以j为终点长度不大于k的和最大的序列了。(这样避免了O(NK)的求和复杂度)将原问题转化为这样一个问题后,就可以用单调队列解决了。(!!!!!求出sum[i](i=1,2,3...n)并不需要O(n^2),只需要O(1)啊,因为可以存储sum[i-1],那么sum[i]=sum[i-1]+a[i])
单调队列即保持队列中的元素单调递增(或递减)的这样一个队列,可以从两头删除,只能从队尾插入。单调队列的具体作用在于,由于保持队列中的元素满足单调性,对于上述问题中的每个j,可以用O(1)的时间找到对应的s[i]。(保持队列中的元素单调增的话,队首元素便是所要的元素了)。
维护方法:对于每个j,我们插入s[j-1](为什么不是s[j]? 队列里面维护的是区间开始的下标,j是区间结束的下标),插入时从队尾插入。为了保证队列的单调性,我们从队尾开始删除元素,直到队尾元素比当前需要插入的元素优(本题中是值比待插入元素小,位置比待插入元素靠前,不过后面这一个条件可以不考虑),就将当前元素插入到队尾。之所以可以将之前的队列尾部元素全部删除,是因为它们已经不可能成为最优的元素了,因为当前要插入的元素位置比它们靠前,值比它们小。我们要找的,是满足(i>=j-k+1)的i中最小的s[i],位置越大越可能成为后面的j的最优s[i]。
在插入元素后,从队首开始,将不符合限制条件(i>=j-k+1)的元素全部删除,此时队列一定不为空。(因为刚刚插入了一个一定符合条件的元素)
<pre name="code" class="cpp">#include <iostream> #include <cstdio> #include <deque> using namespace std; const int maxn=100010; int a[maxn]; int sum[2*maxn]; int main(int argc, char const *argv[]) { int t,n,k; cin>>t; while(t--){ deque<int> Q; cin>>n>>k; sum[0]=0; int omax=-99999,obegin,oend; for(int i=1;i<=n;i++){scanf("%d",&a[i]);sum[i]=sum[i-1]+a[i];} for(int i=n+1;i<n+k;i++) sum[i]=sum[i-1]+a[i-n]; for(int i=1;i<n+k;i++){ while(!Q.empty()&&sum[Q.back()]>=sum[i-1]) Q.pop_back(); while(!Q.empty()&&i-Q.front()>k) Q.pop_front(); Q.push_back(i-1); if(omax<sum[i]-sum[Q.front()]){ omax=sum[i]-sum[Q.front()]; obegin=Q.front()+1; oend=i; } } if(oend>n) oend-=n; printf("%d %d %d\n",omax,obegin,oend); } return 0; }
hdoj3530 Subsequence
这题需要很巧妙地想到套用单调队列,刚开始范神给我提供了一个思路:
尺取法,顾名思义,像尺子一样,一块一块的截取。这样需要怎么做呢?就是把左边的数字作为参考数字
while(scanf("%d%d%d",&n,&m,&k)==3){ int ans=-1; int right=-1; for(int i=0;i<n;i++) scanf("%d",&a[i]); for(int left=0;left<n;left++){ if (right<left) { minn=INF; maxn=-INF; right=left-1; }//初始化 while (right<n-1&&max(maxn,a[right+1])-min(minn,a[right+1])<=k) { right++; maxn=max(maxn,a[right]); minn=min(minn,a[right]); } if (maxn-minn>=m) ans=max(right-left+1,ans); } // for(int i=0;i<n;i++) printf("%d ",a[i]); // cout<<endl; cout<<ans<<endl; }这样做是怎么想的?把左边的数字作为参考点,从左往右扫,同时维护最大最小值,直到到达一个位置,这个位置最大值减去最小值>k (注意条件里面的
a[right+1]<=k,这样的目的是先判断再移动)
然后判断这个值是否在m和k之间。
但是这里问题是:移动过程以后最大最小值的性质被破坏掉了。如果a[left]不是最大值或者最小值,那么这样做是没有问题的。但是如果最大值是a[left],那么我
们就要找到a[left]到a[right]之间的最大值和最小值,甚至如果a[left+1]还是一个次大次小值,那么记录一个次大次小就不够,那么我们就必须记录每个值,这个
时候就必须使用单调队列。
单调队列的做法
再次明白一下单调队列的性质:单调队列不能提供最长递降子序列(最长递降子序列的算法至少是O(nlogn),而单调队列是O(n))。但是注意到单调队列有两个性质:下标的单调递增性质,这样导致了后进的必然是最优的。其次是越大的越优,这保证了扫完一遍数组中最大的必然在单调队列的队首(单调递降序列)。所以单调队列实际上寻找到了一个以最大元素为首的递降子序列。
在这里,以上的程序是不够的,因为缺乏对于min,max的更新,我们只要加上用单调队列维护的最大最小值更新就行了。
这里要注意单调队列的作用只是维护最大值和最小值。我们现在考察几种情况。
1.当区间内最大值减去最小值小于m,继续移动右指针寻找;
2.当区间内最大值减去最小值在[m,k]之间,和ans比较
3.当区间内最大值减去最小值大于k,那么找出他们中较小的min,左指针移动到min+1;
不知道为什么下面的程序没过。。思路应该没错。
#include <iostream> #include <cstdio> #include <algorithm> #include <deque> using namespace std; const int maxn=100010; const int INF=0x99999; int a[maxn]; int max2(int a,int b){ return a>b?a:b; } int main(int argc, char const *argv[]) { int n,m,k; while(scanf("%d %d %d",&n,&m,&k)==3){ int ans=-INF; for(int i=1;i<=n;i++) scanf("%d",&a[i]); deque<int> Q1; deque<int> Q2; int left=1; for(int i=1;i<=n;i++){ while(!Q1.empty()&&a[Q1.back()]<=a[i]) Q1.pop_back(); Q1.push_back(i); while(!Q2.empty()&&a[Q2.back()]>=a[i]) Q2.pop_back(); Q2.push_back(i); if(a[Q1.front()]-a[Q2.front()]<m) continue; while(a[Q1.front()]-a[Q2.front()]>k){ if(Q1.front()<=Q2.front()) {left=Q1.front()+1;Q1.pop_front();} else {left=Q2.front()+1;Q2.pop_front();} } if(!Q1.empty()&&!Q2.empty()) ans=max2(ans,i-left+1); } cout<<ans<<endl; } return 0; }