题解:P10478 生日礼物
bianshiyang · · 题解
题目传送门
本篇篇幅较长,细节处证明居多,是本蒟蒻刚开始学习反悔贪心的笔记,希望能让很多初学者更好理解一点,也希望各位大佬可以耐心看完。
分析
感觉有点贪心的味道,但是没啥办法可以一直贪下去,于是考虑 DP,结果发现由于取的块数的限制,使得设出来的状态是二维的,即
不得不说,发明反悔贪心的人绝对是个天才。
建议之前没有做过反悔贪心的可以去看看这题,因为这个东西确实很奇妙,第一次接触可能不太好理解。
反悔贪心,顾名思义就是说在贪心的过程中可以进行撤回之前某一步操作,这也使得这个算法应用场景并不是很多,因为绝大多题目是很难进行撤回操作的,而且这个算法套路性也比较强。
一般撤回操作要和贪心操作是类似的,这样子我们直接进行贪心过程就可以不用考虑哪几步要贪心,哪几步要撤回,一直做下去就是最优的。常用数据结构就是堆(这里也叫做反悔堆)和链表(支持快速插入删除元素)等,其实直接用 set
代替好像更方便点吧。
至于哪些题可以使用反悔贪心,还需要多刷题找感觉。不过有一类题目大概率可以用反悔贪心,就是只贪心一步是好想的,可能会分几类,然后之后就很复杂,但是撤回操作也是好想的,也就是说可以找到一个等价的东西把第一步换回来,且所耗费的代价与操作的代价相等,或者说可以在第一步分的某一类里找到与第一步相反的操作。可能觉得云里雾里?看看在这题怎么体现的。
回到本题,一个比较显然的贪心就是对于一大段正数或者一大段负数肯定要么一起选,要么一起都不选。原因就是如果一大段正数不选完,那么选完它会更优;而如果一大段负数不选完,那么不选它一定更优,其实选负数是为了把与它相邻的两正数段合并在一起,所以这种情况下一定要把这个负数段全部选完更优(不然正数段无法合并在一起)。所以我们可以把所有连续的正数合并在一起,所有连续的负数合并在一起。接下来的叙述都是建立在合并完的序列上,也就是序列里正数与负数交替出现,将这个序列记为
想到这里好像没啥思路了,但是可以发现如果记正数个数为
而如果
若记所有正数和为
此时我们可以考虑撤回操作了,对于第一类操作,撤回的意义在于
那么我们可以每次进行操作一之后把其两侧的负数
对于第二类操作的撤回操作是同理的。可以证明每一个进行操作的
至此,反悔贪心的证明就全部结束,希望这大段证明能对大家有帮助。
代码实现
有几处细节需要说明,首先对于原序列中的
#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
int n,m,a[N],b[N],cnt,ans;
int pre[N],nxt[N];//链表
bool vis[N];
priority_queue<PII,vector<PII>,greater<PII> > q;//小根反悔堆,这里用set也可以
void Del(int x)//删除操作
{
if(x<1||x>n) return;//因为堆里面特判了,所以这行删去也可以A
vis[x]=1;//堆的懒惰删除法,和Dijkstra的删除原理差不多
nxt[pre[x]]=nxt[x];
pre[nxt[x]]=pre[x];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
if(!a[i]) continue;
if(i==1||(long long)a[i]*b[cnt]<0) b[++cnt]=a[i];//这里a[i]*b[cnt]不可以写成a[i]*a[i-1],因为可能a[i-1]是0,就会挂
else b[cnt]+=a[i];
}//b中存储的是合并后的值
n=cnt;cnt=0;
for(int i=1;i<=n;i++) a[i]=b[i];
for(int i=1;i<=n;i++)//堆和链表初始化
{
pre[i]=i-1,nxt[i]=i+1;
q.push(make_pair(abs(a[i]),i));
if(a[i]>0) ans+=a[i],cnt++;
}
while(cnt>m)
{
PII u=q.top();q.pop();
if(vis[u.se]) continue;
if((pre[u.se]>=1&&nxt[u.se]<=n)||a[u.se]>0)//边界处的负数直接不用管,这个不可以在贪心之前直接判掉
{
ans-=u.fi;cnt--;
a[u.se]+=a[pre[u.se]]+a[nxt[u.se]];
q.push(make_pair(abs(a[u.se]),u.se));//这里记得加上绝对值
Del(pre[u.se]);Del(nxt[u.se]);//删除左右节点
}
}
printf("%d\n",ans);
return 0;
}