学习笔记——反悔贪心
前言
发现自己并不是很熟练这个人类智慧,于是来补一下。
反悔贪心
其本质大概是考虑一个有一个能被 hack 的贪心策略,这个时候我们先选着当前策略,然后我们考虑加入一个东西,使得之后选这个东西可以抵消掉之前的策略。
一般考虑用堆实现这个东西。感觉和 dp 一样是人类智慧。具体可以看看例题。
例题
有 \(n\) 天,每天可以买入一支卖出一支,或者什么都不做,求 \(n\) 天后能获得的最多的钱。
容易发现,一天内买入一次加上卖出一次相当于什么都没做,于是我们考虑一个贪心,就是每天选择之前价格最低的与之配对,也就是那天买入,这天卖出,这个用一个小根堆维护即可。
但是这个东西显然是错的,因为可能这个最小价格的留给后面某一个配对是更优的,所以我们考虑消除当前的选择,也就是所谓的反悔。
比如说,我们之前选择了 \(i,j\) 配对,但是当枚举到 \(k\) 的时候,你发现,可能 \(i,k\) 配对更优。这个怎么撤销呢?我们发现:
就是说,相当于抉择了两次。考虑实际上就是让 \(j\) 配对了之后还能与 \(k\) 配对,所以就是再次把 \(c_j\) 加入小根堆就好了。
My Code
const int MAXN=3e5+10;
priority_queue<int,vector<int>,greater<int>> q;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
int n,v;cin>>n;
ll ans=0;
rep(i,1,n){
cin>>v;
if(!q.empty()&&q.top()<v)
ans+=v-q.top(),q.pop(),q.push(v);
q.push(v);
}cout<<ans<<'\n';
return 0;
}
从这个例题中我们可以总结出反悔贪心的一般套路。就是首先考虑贪心,然后发现这贪心是假的;然后考虑题目中一个元素的『无用』操作,即正着做一次反着做一次相当于没做;然后考虑贪心哪里是错的;最后考虑用加入反元素的方式来撤销之前的抉择。
还是要多做题。
练习题
有 \(n\) 项工作(\(1\le n\le 10^5\)),每项工作有截止时间和利润。完成一项工作可以获得它的利润,而每一时刻只能选择完成一项工作。求最多获得的利润。
错误的贪心,就是不考虑优劣性,直接往里面加。那我们考虑维护一个堆,然后按时间排序后依次加入,这样感性理解起来可以加入很多。然后反悔的时候,取出当前堆顶,然后比较当前元素于堆顶的优劣。如果是当前元素更优,那就把之前做堆顶工作的时间拿来做当前工作。
有 \(n\) 个建筑,每个建筑有时间花费 \(t_1\) 和截止时间 \(t_2\)。求最多抢修几个建筑(一个修完才能修下一个)。
这题变成了价值为 \(1\) 而用时给出。同样考虑错误的贪心,按时间顺序加入,然后每次找前面占时间最多的来替换,需要大根堆。
现在数轴上有 \(n\) 个点,从原点出发,每一单位时间可以走一单位距离。到达一个点可以选择花费 \(t_i\) 的时间获得 \(1\) 的价值。求最终能获得多少价值。
首先一个贪心是肯定不会往回求,这要求我们边走边做好策略的选择。这样直接反悔即可。和上面那题一样顺次加入,然后如果无法加入,就把之前花费时间最多的点舍弃掉。
你经营一家商店,上午会进货 \(a_i\),下午可以选择卖出 \(b_i\),从而满足一位顾客。求最多满足多少顾客。并输出方案。
又是非常显然的反贪。你首先考虑贪心,如果能卖那就一定卖。然后如果某一天商品不够了,就从前面选一个买了商品最多的人,替换掉。
现在有 \(n\) 个树坑,每个坑种树有一定收益。现在有 \(k\) 棵树可以种,然后两棵树不能相邻,求最大收益。
比较难想啊,看了题解。就是你首先考虑一个错误的贪心,就是每次取出最大的一个数,如果它左右没有种,那就种上,否则就不种。考虑这样在什么时候是不优的。就是你如果选了一个数,结果把这个数换成选两边可能更加优。所以你考虑加入一个东西使得能够反悔。
其实和例题差不多,就是你希望可以选两边的而不选中间,那你就加入 \(w_l+w_r-w_{mid}\),这样,你把序列用链表维护起来,然后选了一个点就把它左右的数从链表中删去,同时把这个点的权值改成上面说的那个。然后每次在大根堆中不断取出,看能不能种。
好强的反悔方式!讨论区 \(4\) 倍经验!
有 \(n\) 个人,每个人有两个属性 \(a,b\),把这 \(n\) 个人分成 \(A,B\) 两个大小分别为 \(p,s\) 的集合。使得 \(A\) 集合中的人 \(a\) 属性的和加上 \(B\) 集合中的人 \(b\) 属性的和的和最大。输出方案。
考虑一个贪心,就是首先先把前 \(p\) 个 \(a\) 大的放到 \(A\) 集合中的。然后看每一个放到 \(B\) 中对最终答案的贡献,看是否更优,如果能更优就加。然后看当前选来放到 \(B\) 集合的人,如果没有被选入到 \(A\) 中,那么直接加入,答案加上 \(b_i\)。否则,我们看能不能找一个人来替代它更优,我们假设是 \(j\),那么对答案的贡献是 \(b_i-a_i+a_j\)。如果这个贡献是负的,那我当前这个不加也罢!
所以我们当前的策略是,把没有加入 \(B\) 的人的 \(b_i,a_i,b_i-a_i\) 都塞到堆里面。然后每次取出 \(b_i\) 的堆中最大的和 \(b_i-a_i\) 中最大的加上 \(a_i\) 中最大的。注意,\(b_i\) 和 \(a_i\) 堆中是两个集合都没有选的,剩下的是 \(b_i-a_i\) 是选入了 \(A\) 集合的。然后两个比较一下,加入即可。这样循环 \(s\) 次,就求出最后的结果了。注意及时更新堆中的元素。
就是在上面那题的基础上再加上多出一个属性。并且每个人都要属于一个集合。
考虑转化成上一题。你假设大家都加入第一个集合,那么然后 \(b_i\to b_i-a_i\),\(c_i\to c_i-a_i\)。就和上一题一模一样了。
每天可以花费 \(a_i\) 准备一题,花费 \(b_i\) 打印一题,然后每天最多准备一题打印一题,求打印 \(k\) 题的最小花费。
首先考虑贪心,你可以把这个准备看成是买入一只股票,打印是反向卖出一只股票。现在需要求恰好卖出 \(k\) 只股票。如果没有 \(k\) 只股票的限制,则直接使用例题的思想就做完了。现在,我们考虑,如何控制它恰好卖出 \(k\) 只股票。直观考虑,如果我们每只股票的收益同时增加一个 \(d\),则最终策略的优劣性不变,但是我们会买更多的股票。于是我们二分这个 \(d\),然后每次求最优策略下买的股票的个数就行了。
\(n\) 个关卡,对每个关卡,你可以花 \(a_i\) 代价得到一颗星,也可以花 \(b_i\) 代价得到两颗星,也可以不玩。问获得 \(w\) 颗星最少需要多少时间。并输出方案。
同样考虑贪心,考虑加入的时候贪心有两种方式,一种是一颗星星,另一种是两颗星星。不妨考虑把两颗星星当成花费 \(b_i-a_i\) 的代价在一颗星星的基础上在获得一颗星星。我们进行反悔,可以把一颗星星反悔成不选,然后再另外选一颗变成两星,那么就是 \(b_j-a_i\)。也可以把两颗星星的反悔成一颗,然后再另外选一个没有星星的变成两颗星星,这样代价就是 \(a_i-b_i+b_j\)。然后我们用 \(5\) 个堆分别维护 \(b_i-a_i\),\(b_i\),\(-a_i\),\(a_i-b_i\),\(a_i\)。
后记
其实做多了就发现,反悔贪心的最重要的一点就是找到撤销与贪心。
然后迫使每次选择都使决策点加一。