反悔贪心
目录:
- 个人理解
- 反悔贪心的分类
- 反悔自动机
- 反悔堆
- 例题简析及代码
一、个人理解:
贪心本身是没有反悔操作的,贪心求的就是当前的最优解。但当前的最优解有可能是局部最优解,而不是全局最优解,这时候就要进行反悔操作。
反悔操作指的是这一步的贪心不是全局最优解,我们就退回去一步(人工或自动判断),换一种贪心策略。按照判断方式的不同可以分为反悔自动机和反悔堆两种方法。
二、反悔贪心的分类:
-
反悔自动机:
即设计一种反悔策略,使得随便一种贪心策略都可以得到正解。
基本的设计思路是:每次选择直观上最接近全局最优解的贪心策略,若发现最优解不对,就想办法自动支持反悔策略。(这就是自动机的意思)
具体题目具体分析。一般需要反悔自动机的题都是通过差值巧妙达到反悔的目的。
-
反悔堆:
即通过堆(大根堆、小根堆)来维护当前贪心策略的最优解,若发现最优解不对,就退回上一步,更新最优解。
由于堆的性质,使得堆的首数据一定是最优的,这就可以实现快速更新最优解。
三、例题简析及代码
-
USACO09OPEN 工作调度Work Scheduling (反悔堆)
Description:
有 \(n\) 项工作,每 \(i\) 项工作有一个截止时间 \(D_i\) ,完成每项工作可以得到利润 \(P_i\) ,求最大可以得到多少利润。
Method:
做这道题的时候并没有想到反悔贪心,只是想到一个错误的贪心算法。按照截止时间为第一关键字,利润为第二关键字排序,统计一遍即可。
显然上面的贪心算法刻印被Hack掉。可以先不选择当前截止时间的利润,等一下选择下一个更大的利润,这样可以得到更大的最优解。
但我们发现这个贪心策略错误的原因是当前的最优解可能不是全局最优解,显然符合反悔贪心的思想。于是我们用一个反悔堆维护最优解。
假如满足题设条件(即没有超出截止时间)就分成两种情况:若当前的最优解比原来的最优解(堆顶)更优秀,我们就更新全局最优解,把原来的最优解丢出去,再把当前的最优解放进去(即反悔策略);反之,就不管了。假如不满足特设条件,就把当前的最优解丢进堆里,更新全局最优解即可。
Code:
#include<bits/stdc++.h> #define int long long #define Maxn 100010 inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } using namespace std; int n; struct node { int D,P; bool operator <(const node &x)const { return D<x.D; } }job[Maxn]; priority_queue<int,vector<int>,greater<int> >qu; signed main() { // freopen("Job.in","r",stdin); // freopen("Job.out","w",stdout); read(n); for(int i=1;i<=n;i++) { read(job[i].D),read(job[i].P); } sort(job+1,job+n+1); int ans=0; for(int i=1;i<=n;i++) { if(qu.size()>=job[i].D)//符合条件 { if(qu.top()<job[i].P)//当前的最优解比原来的最优解(堆顶)更优秀 { ans-=qu.top();//更新全局最优解 qu.pop();//把原来的最优解丢出去 qu.push(job[i].P);//把当前的最优解放进去 ans+=job[i].P;//更新全局最优解 } }else//不符合条件 { qu.push(job[i].P);//把当前的最优解丢进堆里 ans+=job[i].P;//更新全局最优解 } } printf("%lld",ans); return 0; }
-
CF865D Buy Low Sell High(反悔自动机)
Description:
已知接下来 \(n\) 天的股票价格,每天可以买入当天的股票,卖出已有的股票,或者什么都不做,求 \(n\) 天之后最大的利润。
Method:
我们可以快速想出一种贪心策略:买入价格最小的股票,在可以赚钱的当天卖出。
显然我们可以发现,上面的贪心策略是错误的,因为我们买入的股票可以等到可以赚最多的当天在卖出。
我们考虑设计一种反悔策略,使所有的贪心情况都可以得到全局最优解。(即设计反悔自动机的反悔策略)
定义 \(C_{buy}\) 为全局最优解中买入当天的价格, \(C_{sell}\) 为全局最优解中卖出当天的价格,则:
\[C_{sell}-C_{buy}=\left(C_{sell}-C_i\right)+\left(C_i-C_{buy}\right) \]\(C_i\) 为任意一天的股票价格。
即我们买价格最小的股票去卖价格最大的股票,以期得到最大的利润。我们先把当前的价格放入小根堆一次(这次是以上文的贪心策略贪心),判断当前的价格是否比堆顶大,若是比其大,我们就将差值计入全局最优解,再将当前的价格放入小根堆(这次是反悔操作)。相当于我们把当前的股票价格若不是最优解,就没有用,最后可以得到全局最优解。
上面的等式即被称为反悔自动机的反悔策略,因为我们并没有反复更新全局最优解,而是通过差值消去中间项的方法快速得到的全局最优解。
(假如还没有理解这道题,可以看一看代码,有详细的注释)
Code:
#include<bits/stdc++.h> #define int long long using namespace std; inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } priority_queue<int,vector<int>,greater<int> >qu;//开一个小根堆 int n; int ans=0;//全局最优解 signed main() { read(n); ans=0; for(int i=1,x;i<=n;i++) { read(x);//当前的股票价格 qu.push(x);//贪心策略:买价格最小的股票去买价格最大的股票 if(!qu.empty()&&qu.top()<x)//假如当前的股票价格不是最优解 { ans+=x-qu.top();//将差值计入全局最优解 qu.pop();//将已经统计的最小的股票价格丢出去 qu.push(x);//反悔策略:将当前的股票价格再放入堆中,即记录中间变量(等式中间无用的Ci) } } printf("%lld\n",ans);//输出全局最优解 return 0; }
-
BZOJ2151 种树(反悔自动机)
Description:
有 \(n\) 个位置,每个位置有一个价值。有 \(m\) 个树苗,将这些树苗种在这些位置上,相邻位置不能都种。求可以得到的最大值或无解信息。
Method:
先判断无解的情况,我们显然可以发现,若 \(n<\frac{2}{m}\) ,则是不能在合法的条件下种上 \(m\) 棵树的,故按题意输出
Error!
即可。假如有解的话,我们可以很轻松的推出贪心策略:在合法的情况下选择最大的价值。
显然上面的策略是错误的,我们选择了最大价值的点,相邻的两个点就不能选,而选择相邻两个点得到的价值可能更大。
考虑如何设计反悔策略。
我们同样用差值来达到反悔的目的。假设有 \(A\) ,\(B\) ,\(C\) ,\(D\) 四个相邻的点(如图)。
\(A\) 点的价值为 \(a\) ,其他点同理。若:
\[a+c>b+d \]则:
\[a+c-b>d \]假如我们先选了 \(B\) 点,我们就不能选 \(A\) 和 \(C\) 两点,这显然是不对的,但我们可以新建一个节点 \(P\) , \(P\) 点的价值为 \(a+c-b\) ,再删去 \(B\) 点。(如图,红色的是删去的点,橙色的新建的点)
下一次选择的点是 \(P\) 的话,说明我们反悔了(即相当于 \(B\) 点没有选),可以保证最后的贪心最优解是全局最优解。
如何快速插入 \(P\) 点和找出是否选择 \(P\) 点呢?我们可以使用双向链表和小根堆,使得最终在 \(O(n\log n)\) 的时间复杂度下快速求出全局最优解。
Code:
#include<bits/stdc++.h> #define int long long #define Maxn 2000010 using namespace std; inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } int n,m; int w[Maxn],lft[Maxn],rgh[Maxn]; struct node { int val,id; bool operator <(const node &n) const { return val<n.val; } }; priority_queue<node>qu; int ind,ans=0; int vis[Maxn]; signed main() { read(n),read(m); ind=n; if(n/2<m) { puts("Error!"); return 0; } for(int i=1;i<=n;i++) { read(w[i]); node tmp; tmp.id=i; tmp.val=w[i]; qu.push(tmp); if(i==1) { lft[i]=n; rgh[i]=i+1; }else if(i==n) { lft[i]=i-1; rgh[i]=1; }else { lft[i]=i-1; rgh[i]=i+1; } } for(int i=1;i<=m;i++) { while(vis[qu.top().id]) qu.pop(); int id=qu.top().id; int val=qu.top().val; qu.pop(); ans+=val; ind++; vis[lft[id]]=vis[rgh[id]]=1; lft[rgh[rgh[id]]]=ind;rgh[lft[lft[id]]]=ind; lft[ind]=lft[lft[id]];rgh[ind]=rgh[rgh[id]]; w[ind]=w[lft[id]]+w[rgh[id]]-val; int newid=ind; int newval=w[ind]; node tmp; tmp.id=newid; tmp.val=newval; qu.push(tmp); } printf("%lld\n",ans); return 0; }
Warning:
- 一定要记录这个点选没有选过,假如已经选过了,就从堆中丢出去;
- 1与 \(n\) 是相邻的,一定要特判一下;
- 双向链表一定不要写挂了;
- 一定要先将新建的点的价值存入一开始的价值数组,再丢进堆里;(卡在45卡了好久)
index
是关键字,一定不要使用。(我成功CE了一次)