您的位置:首页 > 其它

[单调队列] day.1

2016-07-12 09:17 169 查看

单调队列的操作

举例

不妨用一个问题来说明单调队列的作用和操作:

不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。

最直接的方法:普通队列实现缓存数组。

进队出队都是O(1),一次查询需要遍历当前队列的所有元素,故O(n)。

用堆实现缓存数组

堆顶始终是最小元素,故查询是O(1)。

而进队出队,都要调整堆,是O(log(n))。

RMQ的方法

RMQ即Range Maximum(Minimum) Query,用来求某个区间内的最大值或最小值。使用线段树或稀疏表是O(log(n))级的。对于这类问题这两种方法也搞得定,但是没有单调队列快。

单调队列的舞台

由于单调队列的队头每次一定最小值,故查询为O(1)。

进队出队稍微复杂点:

进队时,将进队的元素为e,从队尾往前扫描,直到找到一个不大于e的元素d,将e放在d之后,舍弃e之后的所有元素;如果没有找到这样一个d,则将e放在队头(此时队列里只有这一个元素)。

出队时,将出队的元素为e,从队头向后扫描,直到找到一个元素f比e后进队,舍弃f之前所有的。(实际操作中,由于是按序逐个出队,所以每次只需要出队只需要比较队头)。

每个元素最多进队一次,出队一次,摊排分析下来仍然是 O(1)。

上面的话可能还是没能讲出单调队列的核心:队列并不实际存在的,实际存在的是具有单调性的子序列。对这个子序列按心中的队列进行操作,譬如在进队时丢弃的元素,虽然它不存在于这个子序列里,但是还是认为他存在于队列里。

另外,进队的顺序和出队的顺序并不一定相同,因为这个队列本身是隐含存在的,可以在进队时看成一个队列,出队时看成另一个队列,只要出队的元素在队列中就行。可以想象成一个队列只有头和身,另一个队列只有身和尾,而这身是共用的。

在OI赛场上,大多数题目为单调队列力所不能及的,取而代之的是单调队列基础上改进的斜率优化,单调栈等,因为其限制条件,故潜力不大。但需要掌握,因为有许多算法建立在其基础上。

例如斜率优化即为f[i] = min/max{f[j] + g[i] * g[j]},和单调队列尤为相似。

单调栈即为单调队列的“栈”版。

这两种复杂度也是O(n)的。

在信息学竞赛的一些应用

编辑

动态规划·单调队列的理解

做动态规划时常常会见到形如这样的转移方程:

f[x] = max or min{g(k) | b[x] <= k < x} + w[x]

(其中b[x]随x单调不降,即b[1]<=b[2]<=b[3]<=…<=b
)

(g[k]表示一个和k或f[k]有关的函数,w[x]表示一个和x有关的函数)

这个方程怎样求解呢?我们注意到这样一个性质:如果存在两个数j, k,使得j <= k,而且g(k) <= g(j),则决策j是毫无用处的。因为根据b[x]单调的特性,如果j可以作为合法决策,那么k一定可以作为合法决策,并且k是一个比j要优的决策。因为k比j要优,(注意:在这个经典模型中,“优”是绝对的,是与当前正在计算的状态无关的),所以,如果把待决策表中的决策按照k排序的话,则g(k)必然是不降的。在此例中决策表即f[x].

这样,就引导我们使用一个单调队列来维护决策表。对于每一个状态f(x)来说,计算过程分为以下几步:

1、 队首元素出队,直到队首元素在给定的范围中。

2、 此时,队首元素就是状态f(x)的最优决策,

3、 计算g(x),并将其插入到单调队列的尾部,同时维持队列的单调性(不断地出队,直到队列单调为止)。

重复上述步骤直到所有的函数值均被计算出来。不难看出这样的算法均摊时间复杂度是O(1)的。因此求解f(x)的时间复杂度从O(n^2)降到了O(n)。

单调队列指一个队列中的所有的数符合单调性(单调增或单调减),在信息学竞赛的一些题目上应用,会减少时间复杂度

例题1:广告印刷(ad.pas/c/cpp)

【问题描述】

afy决定给TOJ印刷广告,广告牌是刷在城市的建筑物上的,城市里有紧靠着的N个建筑。afy决定在上面找一块尽可能大的矩形放置广告牌。我们假设每个建筑物都有一个高度,从左到右给出每个建筑物的高度H1,H2…HN,且0

vartemp,ans:int64;
n,p,q,i,j:longint;
a:array[0..400000]of longint;
b,r,l:array[0..400000]of longint;
begin
fillchar(b,sizeof(b),0);
readln(n);
for i:=1 to n do read(a[i]);
p:=1;
q:=0;
for i:=1 to n+1 do begin
while (p<=q) and (a[i]<a[b[q]]) do begin
r[b[q]]:=i;
dec(q);
end;
inc(q);
b[q]:=i;
end;

fillchar(b,sizeof(b),0);
p:=1;
q:=0;
for i:=n downto 0 do begin
while (p<=q) and (a[i]<a[b[q]]) do begin
l[b[q]]:=i;
dec(q);
end;
inc(q);
b[q]:=i;
end;
for i:=1 to n do begin
temp:=(r[i]-l[i]-1)*a[i];
if temp>ans then ans:=temp;
end;

writeln(ans);
end.


测试数据

样例输入1【ad.in】

20

12 8 8 30 40 32 19 22 12 32 30 45 15 1937 5 5 6 26 35

样例输出1 【ad.out】

144

样例输入2 【ad.in】

56

3000 2000 180 190 2890 2900 3120 450560 500 300 2100 2300 480 840 880 890 350 550 450 760 960 860 250 260 1050 11301140 2140 2045 2065 3075 3155 3255 3470 3490 3240 920 930 900 930 980 890 740760 770 825 845 855 950 1980 880 680 690 2380 2390

样例输出2【ad.out】

21080

poj_2823_单调队列

【问题描述】

有N个数,每次从左到右选取M个数,第一行选取每个区间中的最小值输出,第二行选取最大值并输出。

【解题思路】

这个是典型的固定k区间的单调队列。套用的本质思想是,如求最小值: 考虑这样的一个问题,在某个区间当中如果存在某两个元素A,B,满足A的下标小于B的下标,A的值大于B的值,那么A这个数就可以删掉,不再考虑。求最大值反之。

具体的操作是:从加入第k个数开始,每插入做一次队列单调性更新:

删队尾【单调性】,入队,删队首【下标范围k以内】,输出队首【即最值】。

需要注意的是,这里用纯单调队列会超时,把删队尾的操作改成二分的话就过了。

#include<stdio.h>
#include<stdlib.h>
#defineN1000001
typedefstruct{
intvalue;
intindex;
}QUE;
QUEmin_que
,max_que
;
intn,k,num
,front,rear;
intdelete_rear_inc(intf,intr,intd){
intmid;
while(f<=r){
mid=(f+r)/2;
if(min_que[mid].value==d)
returnmid;
if(min_que[mid].value>d)
r=mid-1;
else
f=mid+1;
}
returnf;}
intdelete_rear_dec(intf,intr,intd){
intmid;
while(f<=r){
mid=(f+r)/2;
if(max_que[mid].value==d)
returnmid;
if(max_que[mid].value>d)
f=mid+1;
else
r=mid-1;
}
returnf;}

main(){
inti;
//
while(
scanf("%d%d",&n,&k)!=
EOF){
scanf("%d%d",&n,&k);
for(i=1;i<=n;i++)
scanf("%d",&num[i]);
//单调队列求最小——维持k区间的递增队列
min_que[1].
value=num[1];
min_que[1].index=1;
front=1;
rear=1;
//1->k
for(i=2;i<=k;i++){
//删队尾,入队
rear=delete_rear_inc(front,rear,num[i]);
min_que[rear].value=num[i];
min_que[rear].index=i;
}

printf("%d",min_que[1].value);
//队首即为第一个滑动窗口的最小值
//k+1->n
for(i=k+1;i<=n;i++){
//删队尾,入队,维持区间范围
rear=delete_rear_inc(front,rear,num[i]);
min_que[rear].value=num[i];
min_que[rear].index=i;
//删队首,维持区间范围
if(i-min_que[front].index>=k)
front++;
//队首为第i-k+1个滑动窗口的最小值

printf("%d",min_que[front].
value);
}
printf("\n");
//单调队列求最大——维持k区间的递减队列
max_que[1].value=num[1];
max_que[1].index=1;
front=1;
rear=1;
//1->k
for(i=2;i<=k;i++){
//删队尾,入队
rear=delete_rear_dec(front,rear,num[i]);
max_que[rear].
value=num[i];
max_que[rear].index=i;
}

printf("%d",max_que[1].value);
//队首即为第一个滑动窗口的最大值
//k+1->n
for(i=k+1;i<=n;i++){
//删队尾,入队,维持区间范围
rear=delete_rear_dec(front,rear,num[i]);
max_que[rear].value=num[i];
max_que[rear].index=i;
//删队首,维持区间范围
if(i-max_que[front].index>=k)
front++;
//队首为第i-k+1个滑动窗口的最大值

printf("%d",max_que[front].
value);
}
printf("\n");
//}

//system("pause");
return0;}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  单调队列