单调队列

底下这道题目,可以用堆来做,虽然我也不知道堆是个啥,但我还是BB一下。但这里,用单调队列来写。

#218. 【单调队列】合并果子

题目描述

  在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。   每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。   因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。   例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。

样例数据

input

3

1 2 9

output

15

再熟悉不过的题目。那是我以前什么算法都没学的时候,想用排序做,也就是每一次都排序一下,最小的两个加起来,然后更新,继续……结果,第一,我自己都搞不清楚哪些是合并过的,哪些是没合并过的;第二,当然会超时咯。因此,我一直过不去这道坎。现在,我终于过去啦~

来,我们分析一下这道题。

#include<bits/stdc++.h>

using namespace std;

int main()

{

int n,a[10010]={},q[10010]={},h1=1,t1,h2=1,t2=0,sum=0;

scanf("%d",&n);

for (int i=1;i<=n;++i)

scanf("%d",&a[i]);

sort(a+1,a+n+1);

t1=n;

for (int i=1;i<n;++i)

{

int min_=2000000000,bj=0;

if (t1>h1&&a[h1]+a[h1+1]<min_)

{

min_=a[h1]+a[h1+1];

bj=1;

}

if (t1>=h1&&t2>=h2&&a[h1]+q[h2]<min_)

{

min_=a[h1]+q[h2];

bj=2;

}

if (t2>h2&&q[h2]+q[h2+1]<min_)

{

min_=q[h2]+q[h2+1];

bj=3;

}

if (bj==1)

{

q[++t2]=min_;

h1+=2;

}

if (bj==2)

{

q[++t2]=min_;

h1++;

h2++;

}

if (bj==3)

{

q[++t2]=min_;

h2+=2;

}

}

for (int i=1;i<=t2;++i)

sum+=q[i];

cout<<sum<<endl;

return 0;

}

这道题目其实用到的是两个队列(我竟然觉得不是单调队列,事实上,是是是,是两个单调递增的队列。

第一个队列,是输入的a数组,进行一下sort排序,就是单调递增的队列了。第二个q队列,是用来存你合并后的果子。

我一开始一直处理不好边界,用什么for循环,while循环,思路都很浑浊。因此GG。但是,你可以发现,这道题目就是哈夫曼树的模拟过程,每次都找剩余的数里最小的两个进行合并。所以,总共循环了n-1次。那你就用一个for循环,循环n-1次。在循环里面,因为合并的过程有三种情况,就开始用if语句进行分类讨论。①队列a中头两个元素进行合并,但是要满足的条件是队列a中有至少两个元素。②队列a的队头和队列q的队头合并,但是要满足队列a和q中都要有一个元素。③队列q中头两个元素合并,但是要满足队列q中至少有两个元素。还有一点,就是这三种情况下要选那个合并的最少的,也就是我代码里的min_就是用来取最小值的。其它应该是没问题的。

最大的一个problem是我一直搞不清楚的队头和队尾问题。对于这个大问题,我认为还是得手模拟一遍,head和tail指向要清楚。这道题里,判断队列里至少有两个元素的语句是tail>head,因为tail指向一个元素,head也指向一个元素,如果tail>tail,就说明至少有两个元素。判断队列里有一个元素的语句是tail>=head,这是同理的。另外,它们的初值问题。h1和h2初值都为1,因为head要指向一个元素,如果它们为0就没有指向的元素了。t1初值为n,因为a是一个只出不进的队列,它的队头在不断的向后移,但是它的队尾是不用改变的。t2初值为0,那是因为q队列里不断的有元素进队,而进队的语句是先自增,所以t2一开始才要等于0。

下一道是正宗的单调队列:

#219. 窗户

题目描述

给你一个长度为 N 的数组,一个长为 K 的滑动的窗体从最左移至最右端, 你只能见到窗口的K个数,每次窗体向右移动一位

你的任务是找出窗口在各位置时的 max value,min value.

输入格式

1 行 n,k, 第 2 行为长度为 n 的数组

输出格式

2 行, 第 1 行每个位置的 min value, 第 2 行每个位置的 max value

样例数据

input

8 3

1 3 -1 -3 5 3 6 7

output

-1 -3 -3 -3 3 3

3 3 5 5 6 7

 

 

 

 

我们模拟一下:

1 3 -1 -3 5 3 6 7

K=3

 

先让我们求一个最小值:(单调递增

①1进队,成为队头;

②3进队;

③-1进队,替代1成为队头,这时i=k=3,开始输出队头3;

④-3进队,替代-1成为队头,输出队头-3;

⑤5进队,输出队头-3;

⑥3进队,替代5,输出队头-3;

⑦6进队,但是因为它和队头的下标之差>=k,所以队头出队,输出队头3;

⑧7进队,输出队头3。

再让我们求一个最大值:(单调递减

①1进队,成为队头;

②3进队,替代1成为队头;

③-1进队,这时i=k=3,开始输出队头3;

④-3进队,输出队头3;

⑤5进队,替代3成为队头,输出队头5;

⑥3进队,输出队头5;

⑦6进队,替代5成为队头,输出队头6;

⑧7进队,替代6成为队头,输出队头7。

 

所以,思路就是:一个for循环枚举每一个数,每一个数都要进队。如果是求最小值,那么如果碰到一个比它小的,就用一个while循环,让前面的某些数出队;如果是求最大值,那么如果碰到一个比它大的,就用一个while循环,让前面的某些数出队。然后每层循环还需要用一个if语句,如果队中已经进过k个数了,那么就要开始输出了,每次都是输出队头。需要注意的一个点是:后来进队的数与队头的下标之差要小于k。如果大于k,就需要让队头出队,始终让队长保持在k。

 

 

 

代码如下:

#include<bits/stdc++.h>

using namespace std;

int n,k,a[1000010],q[1000010],head=1,tail;

int main()

{

scanf("%d%d",&n,&k);

for (int i=1;i<=n;++i)

scanf("%d",&a[i]);

for (int i=1;i<=n;++i)

{

if (i-q[head]>=k) head++;

while (a[i]<a[q[tail]]&&head<=tail)

tail--;

q[++tail]=i;

if (i>=k)

{

if (i==n) cout<<a[q[head]]<<endl;

else cout<<a[q[head]]<<" ";

}

}

memset(q,0,sizeof(q));

head=1;

tail=0;

for (int i=1;i<=n;++i)

{

if (i-q[head]>=k) head++;

while (a[i]>a[q[tail]]&&head<=tail)

tail--;

q[++tail]=i;

if (i>=k)

{

if (i==n) cout<<a[q[head]]<<endl;

else cout<<a[q[head]]<<" ";

}

}

return 0;

}

 

 

 

 

最后一道水题:

#220. holiday

题目描述

经过几个月辛勤的工作,FJ决定让奶牛放假。假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;假期也不能超过Q天,否则奶牛会玩的腻烦。FJ想知道奶牛们能获得的最大享受指数。

输入格式

第一行:N,P,Q. 第二行:N个数字,中间用一个空格隔开。

输出格式

一个整数,奶牛们能获得的最大享受指数。

样例数据

input

5 2 4

-9 -4 -3 8 -6

output

5

Hint 选择第3-4天,享受指数为-3+8=5。

这道题思路有点复杂:第一,你要想到求前缀和,因为假期可以在1…N天内任意选择一段(需要连续)现在我们用sum数组表示前缀和,用minn[i]表示——以i为终点的所有区间的最大享受指数,而j表示——以i为终点的区间的起点范围,所以minn[i]=max{sum[i]-sum[j]},从而转换为求sum[j]的最小值。进一步你可以发现:随着i的往后移动,j也同样往后移动,这样就变成了滑动窗口问题。这样问题就很简单了。

先看代码

#include<bits/stdc++.h>

using namespace std;

long long n,p,q,a[1000010],sum[1000010],qu[1000010],head=1,tail,minn[1000010],maxx=-1e9+7;

int main()

{

scanf("%lld%lld%lld",&n,&p,&q);

for (int i=1;i<=n;++i)

{

scanf("%lld",&a[i]);

sum[i]=sum[i-1]+a[i];

}

for (int i=1;i<=n;++i)

{

if (i-qu[head]>=q-p+1) head++;

while (sum[i]<sum[qu[tail]]&&head<=tail)

tail--;

qu[++tail]=i;

minn[i]=sum[qu[head]];

}

for (int i=p;i<=n;++i)

maxx=max(maxx,sum[i]-minn[i-p]);

cout<<maxx<<endl;

return 0;

}

小结一下这道题:

第一个for循环:输入a数组,求前缀和sum数组。

第二个for循环:滑动窗口,求出每一个以i为终点的区间的最小的sum值。

第三个for循环:枚举每一个终点,计算这个点上的最大享受指数。

ok

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值