单调队列(双端队列) poj2823 hdoj3415 hdoj3530

Posted sentimental_dog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单调队列(双端队列) poj2823 hdoj3415 hdoj3530相关的知识,希望对你有一定的参考价值。

                                             单调队列及其应用(双端队列)

    单调队列,望文生义,就是指队列中的元素是单调的。如:{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;
}


注意:1.为什么是Q.front()+1?因为我们考察的是sum[i],所以序列是从Q.front()+1到i的所有数字和,一共i-Q.front()个数字    2.这里要注意要从i-1开始填入单调队列,这样才能保证从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;
}

坑:http://www.cnblogs.com/neverforget/archive/2011/10/13/ll.html

以上是关于单调队列(双端队列) poj2823 hdoj3415 hdoj3530的主要内容,如果未能解决你的问题,请参考以下文章

POJ 2823 双端队列

poj-2823 单调队列

单调队列 poj2823,fzu1894

poj2823(单调队列)

POJ 2823 Sliding Window(单调队列)

POJ 2823 Sliding Window(单调队列)