您的位置:首页 > 其它

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

2016-05-27 17:09 288 查看


定义:

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

poj2823

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1000010;
int a[maxn];
int dp1[maxn];//dp1是递减
int dp2[maxn];//dp2是递增
int N;
int K;
int main(int argc, char const *argv[])
{
cin>>N>>K;
int front1=1,front2=1;//dp存索引
int tail1=0,tail2=0;//理解front tail的作用:dp1,dp2只是两个队列而已,这是标志头尾指针
for(int i=1;i<=N;i++) scanf("%d",&a[i]);
for(int i=1;i<=K;i++){
while(front1<=tail1 && a[dp1[tail1]]<=a[i]) tail1--;
dp1[++tail1]=i;
while(front2<=tail2 && a[dp2[tail2]]>=a[i]) tail2--;
dp2[++tail2]=i;
}
for(int i=K+1; ;i++){
if(i==N+1){
printf("%d\n",a[dp2[front2]]);
break;
}
printf("%d ",a[dp2[front2]]);
while(front2<=tail2 && i-dp2[front2]+1>K) front2++;
while(front2<=tail2 && a[dp2[tail2]]>=a[i]) tail2--;
dp2[++tail2]=i;
}
for(int i=K+1; ;i++){
if(i==N+1){
printf("%d\n",a[dp1[front1]]);
break;
}
printf("%d ",a[dp1[front1]]);
while(front1<=tail1 && i-dp1[front1]+1>K) front1++;//i到dp[front1]之间有i-dp[front1]+1个数字,这个不等式意思是数字个数大于k
while(front1<=tail1 && a[dp1[tail1]]<=a[i]) tail1--;
dp1[++tail1]=i;
}//dp 存的是索引

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: