什么是反悔贪心
首先,贪心本身没有反悔操作,贪心求的是当前的最优解。但当前的最优解有可能是局部的最优解,而不是全局的最优解,这时候,我们就需要用反悔贪心。如下面的图。如果不进行反悔操作,一直朝着当前局面的最优解走,那么最后很有可能就被困在局部最优解,而不是全局最优解。就像下图中爬山爬到了一个小山顶,但并不是最高的山顶。
但是反悔贪心允许我们在发现现在不是全局最优解的情况下,回退一步或者若干步,去采取另外的策略得到全局最优解。就像站在山峰上,看到了有更高的山峰,那么我们就会先下山,然后去爬那个更高的山峰。
总的来说,反悔操作值的是这一步的贪心不是全局最优解,我们就退回去一步, ( ( (人工或者自动判断 ) ) ),换一种贪心策略,去找全局最优解。一般有两种,反悔堆和反悔自动机。
- 反悔堆:通过堆 ( ( (大根堆,小根堆 ) ) )来维护当前贪心策略的最优解,若发现最优解不对,就退回上一步,更新最优解。由于堆的性质,使得堆的首数据一定是最优的,这样就可以快速更新最优解。
- 反悔自动机:即设计一种反悔策略,使得随便一种贪心策略都可以得到正解。基本的思路是,每次选择直观上最接近全局最优解的贪心策略,如果发现最优解不对,就想办法自动支持反悔策略。一般需要反悔自动机的题都是通过差值巧妙达到反悔的目的。
例题
1. 1. 1.反悔堆
洛谷 P2949 [USACO09OPEN]Work Scheduling G
首先我们可以想到一个贪心,先完成比较紧急的任务,就是 d i d_i di 靠前的,但很明显,如果后面的 p i p_i pi 更大,需要在前面完成,那么这个贪心策略就是错的。
我们考虑如何进行反悔贪心。我们先将工作按截至时间从小到大排序,然后开一个小根堆,首先,如果当前的时间还没满,就是说在当前时间可以工作,那么我们就将这个工作的利润加进 a n s ans ans,同时把这个利润加入小根堆。否则,当此时时间已经满了,也就是说做了的任务数量 ≥ d i \geq d_i ≥di 了,我们就要进行反悔贪心操作。由于堆顶的利润是最小的,所以我们可以将堆顶的利润减掉,然后把当前的利润加进来,这样,既可以保证工作的数量在限制里面,而且也能让答案变得更大。这就是最基础的通过反悔堆实现的反悔贪心。
#include <bits/stdc++.h>
#define re register
#define int long long
#define rep(a,b,c) for(re int a(b) ; a<=c ; ++a)
#define drep(a,b,c) for(re int a(b) ; a>=c ; --a)
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch == '-') f=-1 ; ch=getchar();}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48) ; ch=getchar();}
return x*f;
}
typedef pair<int,int> pii;
const int M = 1e5+10;
int n,ans;
pii a[M];
signed main(){
n = read();
rep(i,1,n) a[i].first = read(),a[i].second = read();
sort(a+1,a+n+1);
priority_queue<int,vector<int>,greater<int> >q;
rep(i,1,n){
if(a[i].first > q.size()) q.push(a[i].second),ans += a[i].second;
else{
if(a[i].second > q.top()){
ans -= q.top();
ans += a[i].second;
q.pop();
q.push(a[i].second);
}
}
}
printf("%lld\n",ans);
return 0;
}
洛谷 P4053 [JSOI2007] 建筑抢修
跟上面那道题类似,也是通过反悔堆来进行反悔贪心。由于我们要尽量让修的建筑修的时间短,所以我们开一个大根堆。先按 T 2 T2 T2 从小到大排序,也就是可以理解为上一道题的截止时间。然后我们要记录修我们修过的建筑一共花了多长时间。如果加上这个建筑修的时间 ≤ \leq ≤ 当前建筑的截止时间,那么这个建筑就可以直接修,把一共修的时间加上这个建筑修需要的时间,同时将这个建筑修需要的时间加入大根堆,并 a n s + 1 ans+1 ans+1。否则,当加上这个建筑修需要的时间已经超过截止时间了,那么我们就要跟堆顶的那个建筑修所需要的时间比较,如果比那个小,那么那个建筑就不修,修当前这个建筑,同时更新一共修的建筑的总共时间。这样我们就完成了反悔贪心。
#include <bits/stdc++.h>
#define re register
#define int long long
#define rep(a,b,c) for(re int a(b) ; a<=c ; ++a)
#define drep(a,b,c) for(re int a(b) ; a>=c ;