一道好的堆题!!!
题目传送门:https://www.luogu.org/problemnew/show/P1484
题意:
给出n个数,求至多选k个数的总和的最大值,要求所选的数两两不能相邻。
思路:
dp很好打,可是时间上过不去。
于是,我们想到(题解想到)这样一种思路:对于n个数中最大值a[i]>0,假设它两边都有数,我们可以想到,在k=1的时候,结果为a[i];而在k=2时,要么a[i-1]和a[i+1]同时被选(即a[i-1]+a[i+1]>a[i]),要么同时放弃(即a[i-1]+a[i+1]<a[i])才能满足贪心,因为如果只选其一的话,它们中的任意一个都比a[i]小,所以最优情况就是选它们两个或者都不选(它们的和可能小于a[i])。
所以综上我们可以考虑到:可以将现在最大数的两边合并,如果它们的和大于最大数,就舍弃最大数,其结果增加value[i-1]+value[i+1]-value[i]。而当整个数列都被过完或最大值已为负数(结果只会负增长)的时候,就结束了。
代码:
(思路看懂了吧,代码却比较难懂)
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
struct node{int v,id;bool operator<(const node &t)const{return v<t.v;}} p;
priority_queue<node> a;
int v[600000],l[600000],r[600000];
bool bz[600000];
int n,k;
long long ans=0;
int main()
{
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&v[i]);
p.v=v[i];//记录当前数的价值
p.id=i;//记录当前数的编号
l[i]=i-1;//假设v[i]为最大数,记录他左边的数的编号
r[i]=i+1;//假设v[i]为最大数,记录他右边的数的编号
a.push(p);//将当前数的价值和编号推入堆中
}
r[0]=1;l[n+1]=n;//将位置0的右边设为1,位置n+1的左边设为n(实际本来就是这样)
memset(bz,true,sizeof(bz));
while(k--)
{
while(!bz[a.top().id]) a.pop();//如果当前数已被访问,就不再访问
p=a.top();//取出当前最大数
a.pop();
if(p.v<0) break;//如果最大数已为负,就break
ans+=p.v;//结果加上当前数的价值
int id=p.id;
v[id]=v[l[id]]+v[r[id]]-v[id];//合并过程
p.v=v[id];//价值改为合并后的价值
bz[l[id]]=bz[r[id]]=false;//因为已经合并,所以最大数的左右两边变为false,下次不用再访问
l[id]=l[l[id]];r[l[id]]=id;//*详见下图
r[id]=r[r[id]];l[r[id]]=id;//*详见下图
a.push(p);//将合并后的编号(不变)和价值推入堆中
}
printf("%lld",ans);
}
PS(最难理解部分,其实也不难,如图):
l[id]=l[l[id]];r[l[id]]=id;//1.合并后,最大值左边变为最大值左边的左边;2.合并后,最大值左边的右边还是最大值
r[id]=r[r[id]];l[r[id]]=id;//2.合并后,最大值右边变为最大值右边的右边;2.合并后,最大值右边的左边还是最大值