底下这道题目,可以用堆来做,虽然我也不知道堆是个啥,但我还是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