Loading

学习笔记——反悔贪心

前言

发现自己并不是很熟练这个人类智慧,于是来补一下。

本文跟随djy 的博客。题目来自于题单

反悔贪心

其本质大概是考虑一个有一个能被 hack 的贪心策略,这个时候我们先选着当前策略,然后我们考虑加入一个东西,使得之后选这个东西可以抵消掉之前的策略。

一般考虑用堆实现这个东西。感觉和 dp 一样是人类智慧。具体可以看看例题。

例题

\(n\) 天,每天可以买入一支卖出一支,或者什么都不做,求 \(n\) 天后能获得的最多的钱。

容易发现,一天内买入一次加上卖出一次相当于什么都没做,于是我们考虑一个贪心,就是每天选择之前价格最低的与之配对,也就是那天买入,这天卖出,这个用一个小根堆维护即可。

但是这个东西显然是错的,因为可能这个最小价格的留给后面某一个配对是更优的,所以我们考虑消除当前的选择,也就是所谓的反悔。

比如说,我们之前选择了 \(i,j\) 配对,但是当枚举到 \(k\) 的时候,你发现,可能 \(i,k\) 配对更优。这个怎么撤销呢?我们发现:

\[c_k-c_i=(c_k-c_j)+(c_j-c_i) \]

就是说,相当于抉择了两次。考虑实际上就是让 \(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\)

后记

其实做多了就发现,反悔贪心的最重要的一点就是找到撤销与贪心。

然后迫使每次选择都使决策点加一。

posted @ 2022-04-30 21:17  ZCETHAN  阅读(171)  评论(0编辑  收藏  举报