单调队列(双端队列) 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

时间: 2024-08-07 08:38:20

单调队列(双端队列) poj2823 hdoj3415 hdoj3530的相关文章

【C/C++学院】0828-STL入门与简介/STL容器概念/容器迭代器仿函数算法STL概念例子/栈队列双端队列优先队列/数据结构堆的概念/红黑树容器

STL入门与简介 #include<iostream> #include <vector>//容器 #include<array>//数组 #include <algorithm>//算法 using namespace std; //实现一个类模板,专门实现打印的功能 template<class T> //类模板实现了方法 class myvectorprint { public: void operator ()(const T &

nyoj1117 鸡蛋队列 (双端队列,deque)

题目1117 题目信息 运行结果 本题排行 讨论区 鸡蛋队列 时间限制:1000 ms  |  内存限制:65535 KB 难度:1 描述 将两根筷子平行的放在一起,就构成了一个队列.将带有编号的鸡蛋放到两根筷子之间叫做入队(push),将筷子之间的鸡蛋拿出来叫做出队(pop).但这两种方式有特殊的定义,对于入队,只能将鸡蛋从队列的尾部向里放入:对于出队,只能将鸡蛋从队列的头部向外将鸡蛋拿出来. 将①.②入队: 头____________尾                         ___

数据结构 --- 01. 时间复杂度,timeit模块,栈,队列,双端队列

一.时间复杂度 1.基本概念 评判程序优劣的方法: 消耗计算机资源和执行效率(无法直观) 计算算法执行的耗时(适当推荐,因为会受机器和执行环境的影响) 时间复杂度(推荐) 时间复杂度 评判规则:量化算法执行的操作/执行步骤的数量 最重要的项:时间复杂度表达式中最有意义的项 大O记法:O(时间复杂度表达式中最有意义的项) 常见的时间复杂度: O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O

PHP双向队列,双端队列代码

<?php /**  * User: jifei  * Date: 2013-07-30  * Time: 23:12 */ /**  * PHP实现双向队列,双端队列  * 双端队列(deque,全名double-ended queue)是一种具有队列和栈性质的数据结构.  * 双端队列中的元素可以从两端弹出,插入和删除操作限定在队列的两边进行.  */ class Deque {     public $queue=array();     /**      * 构造函数初始化队列     

python基础教程_学习笔记19:标准库:一些最爱——集合、堆和双端队列

标准库:一些最爱 集合.堆和双端队列 集合 集合Set类位于sets模块中. >>> range(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> set(range(10)) set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 集合是由序列(或其他可迭代的对象)构建的.主要用于检查成员资格,因此,副本是被忽略的: >>> range(10)*2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9

UVa 210 Concurrency Simulator(双端队列)

题意  模拟程序并行运行 STL队列 双端队列 的应用  用双端队列维护即将执行的程序  再用个队列维护等待变量释放的程序   用lock表示变量锁定状态 先将所有程序依次放到执行队列中  每次取出队首程序运行不超过lim时间  未运行完又放到执行队列队尾 遇到lock时  若当前锁定状态为false就将锁定状态变为true  否则将当前程序放到等待队列队尾并结束运行 遇到unlock时  若等待队列有程序  就将等待队列队首程序放到执行队列队首 遇到end时 退出当前执行(不再进队尾) #in

POJ2823 Sliding Window【双端队列】

求连续的k个中最大最小值,k是滑动的,每次滑动一个 用双端队列维护可能的答案值 如果要求最小值,则维护一个单调递增的序列 对一开始的前k个,新加入的如果比队尾的小,则弹出队尾的,直到新加入的比队尾大,加入队尾 从第k+1个到最后一个,按照上述规则,压入新数,然后弹出队首元素(满足队首元素对应原来序列的位置必须在视窗内,否则,继续弹出下一个) #include <cstdio> #include <cstdlib> #include <iostream> #include

从0开始学算法--数据结构(2.4双端队列与单调队列)

双端队列是特殊的队列,它与队列不同的是可以将元素加入头或尾,可以从头或尾取出元素(滑稽-这部就是栈和队列结合了吗). c++标准库 头文件 #include<deque> 定义 deque<int>deq; 取出队头,尾元素 deq.pop_front(); deq.pop_back(); 访问队头,尾元素 deq.front(); deq.back(); 向队头,尾加入元素 deq.push_front(x); deq.push_back(x); 单调队列是在队列的基础上使它保持

Vijos1834 NOI2005 瑰丽华尔兹 动态规划 单调双端队列优化

设dp[t][x][y]表示处理完前t个时间段,钢琴停留在(x,y)处,最多可以走多少个格子 转移时只需逆着当前倾斜的方向统计len个格子(len为时间区间的长度,len=t-s+1),如果遇到障碍就中断 转移过程可以用单调非递增的双端队列优化 1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 const int maxN=202; 6 const int inf=0x3f3f3f3f