反悔贪心
简介
当贪心策略失效时,可以考虑反悔得到全局最优解,反悔一般用堆实现。
对于一些普通反悔贪心,堆中的元素很容易想到。复杂一些的问题中,我们考虑在 朴素贪心每次确定决策 时再加入一些修正值,以此达到反悔的效果。
关键就是反悔操作的设计以及修正值取啥了,我们可以这样考虑:
其中,
(有差分那味儿了)
例题 CF865D
显然有朴素贪心策略:对于
那么会被
这就很符合反悔贪心的核心式子。
首先有
化简后就有
有些细节和不解还是要自己思考思考的。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n,a,ans;
priority_queue<int,vector<int>,greater<int> >q;
signed main() {
cin>>n;
for(int i=1; i<=n;i++){
scanf("%lld",&a);
if(!q.empty()&&q.top()<a){
ans+=a-q.top(); //计算贡献
q.pop();
q.push(a); //反悔
}
q.push(a);
//注意到ai除了被当作修正值外,自己还可以被买入,所以还要入一次队
}
cout<<ans;
return 0;
}
例题 P1792 [国家集训队] 种树
朴素贪心:将所有点的价值加入大根堆,每次取最大值并标记其两边的点。
错误原因:显然,我们不一定要取这个点,有时取两边的点反而更优。
设当前点为
易想到:重新在队列中加入一个价值为
不过这样做还不够严谨,也许我们不一定取
注意到问题的相似性,我们便有了最终思路:对于
不难发现,这样做恰好包含了所有可能情形。
程序实现中,使用双向链表即可支持上述操作。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,a[N],lt[N],rt[N],ans;
bool vis[N];
priority_queue<pair<int,int> >q;
inline void change(int u){
vis[lt[u]]=vis[rt[u]]=true;
a[u]=-a[u];
if(lt[u]) a[u]+=a[lt[u]];
if(rt[u]) a[u]+=a[rt[u]];
lt[u]=lt[lt[u]]; rt[u]=rt[rt[u]];
rt[lt[u]]=u; lt[rt[u]]=u;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n;i++){
scanf("%d",&a[i]);
q.push({a[i],i});
}
for(int i=1; i<=n;i++){
lt[i]=(i-1==0?n:i-1);
rt[i]=(i+1==n+1?1:i+1);
}
if(n<(m<<1)){
cout<<"Error!";
return 0;
}
while(m&&!q.empty()){
int val=q.top().first,xb=q.top().second;
q.pop();
if(vis[xb]) continue;
--m; ans+=val;
change(xb);
q.push({a[xb],xb});
}
cout<<ans;
return 0;
}