反悔贪心 学习笔记
2022.5.23:为啥这篇这么脑瘫的博客有这么多阅读……
反悔贪心本质就是凸性,广泛应用是费用流。反悔贪心某种意义上可以是在特殊建图下的模拟费用流。
但是这没有啥关系……你知道是模拟费用流也不会做,干脆从贪心角度理解吧!
那么请看下面的内容……
反悔贪心
大家都知道贪心。我们在贪心的过程中,一定保证局部最优解能够得到全局最优解。然而在某些题目中,我们无法从局部最优得到全局最优。这个时候我们应该怎么办呢?
考虑一个反悔的过程,我们需要构造一个方法去消掉之前的贡献转而加为正确的更优的贡献。于是我们不需要管太多,可以直接用局部最优解去扩展,因为反悔机制会帮我们修复成正确的答案,这就是反悔贪心。
至于这个过程,我们一般用堆去实现。
P3620 [APIO/CTSC 2007]数据备份
考虑这个问题。我们需要选择多个点,使得这些点的点权之和最小。要求是选择的点两两不相邻。
根据 \(n \leq 10^6\) 的数据范围,猜测我们需要一个 \(O(n \log n)\) 的做法去解决这个问题。考虑贪心,我们排个序,将最小的点加入我们选择的点集中。如果下一个点和点集中的一个点相邻了,我们就不将这个点加入我们的点集。
显然这个贪心是错误的!考虑一组数据:
2 1 2 9
根据我们的算法,我们会选择 \(1\),然后 \(2,2\) 会被我们直接排除掉,选择 \(9\)。答案为 \(10\),但是显然 \(4\) 更优。怎么办呢?
考虑加入一个新点对应 \(i\),这个点 \(k_i\) 的权值为 \(a_{i-1}+a_{i-2}-a_i\)(表示 \(i\) 这个点左右两个点的权值和减去这个点的权值)。假设我们现在选到一个点 \(1\),我们就把这个点删除,然后再加入一个点 \(3\)。显然我们会跳过 \(2,2\)。这并不是我们想要的结果,但是我们接着会考虑下一个点 \(3\)。答案 \(4\)。
我们惊奇的发现这样就能得到正确结果,原因是我们发现在当前情况下用左右两边的点会比用中间的点更优秀,我们反悔选了另外两边两个,最终得到了正确答案。
不难发现这样是正确的。
于是我们用一个双向链表储存一下当前这个点是否被删除,然后每次选择一个节点,删除它,然后再将 \(k_i\) 加入维护点权堆中,每次取最小值就能做了。这个过程只需要做 \(k\) 次,每次扩展选择一个点即可。
再介绍两个概念:
直接选择,顾名思义。
反悔选择,也就是把之前的直接选择反悔掉,消除掉之前的贡献并扩展成新的贡献。
好像过程听起来很简单,实现起来确实有很多困难。。。所以我把代码贴一下吧。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct node{
LL val,pos;
node(){val=pos=0;}
node(LL V,LL P){val=V,pos=P;}
bool operator < (node another) const {return val>another.val;}
};
priority_queue<node> Q;
LL n,k,pre[100005],nxt[100005],h[100005],dis[100005],ans;
void del(LL x)
{
LL l=pre[x],r=nxt[x];
nxt[x]=nxt[r];
pre[nxt[x]]=x;
pre[x]=pre[l];
nxt[pre[x]]=x;
}
int main(){
scanf("%lld %lld",&n,&k);
for(LL i=1;i<=n;++i) scanf("%lld",&h[i]);
for(LL i=1;i<=n;++i)
{
pre[i]=i-1;
// if(pre[i]==0) pre[i]=n;
nxt[i]=i+1;
// if(nxt[i]==n+1) nxt[i]=1;
dis[i]=h[i+1]-h[i];
}
nxt[n-1]=0;
for(LL i=1;i<n;++i) Q.push(node(dis[i],i));
for(LL i=1;i<=k;++i)
{
node p=Q.top();
Q.pop();
if(p.val!=dis[p.pos])
{
--i;
continue;
}
ans+=p.val;
LL l=pre[p.pos],r=nxt[p.pos];
del(p.pos);
if(l && r) dis[p.pos]=min(1008600100ll,dis[l]+dis[r]-dis[p.pos]);
else dis[p.pos]=1008600100;
dis[l]=dis[r]=1008600100;
Q.push(node(dis[p.pos],p.pos));
}
printf("%lld",ans);
return 0;
}
CF436E Cardboard Box
也是一道有意思的反悔贪心。
我们想一下如何扩展选择一颗星星。可能的情况有哪些呢?
- 我们在一颗星星都没有选择的关卡中选择一颗星星(直接选择);
- 我们在已经选择了一颗星星的关卡中选择两颗星星(直接选择);
- 有一个关卡选了一个,另外有一个关卡一个都没有,于是我们把第一个选择的关卡的星星不要了,然后在第二个选择的关卡选择获得两颗星星(反悔选择);
- 有一个关卡选了两个,另外有一个关卡一个都没有于是我们把第一个选择的关卡的星星变成选一个,然后在第二个选择的关卡选择获得两颗星星(返回操作)。
这样一定是能覆盖掉所有情况的!
于是考虑最后一个问题,我们应该维护什么。我们先考虑上面四个选择的贡献分别是多少。
- 设选择的关卡为 \(i\),贡献为 \(a_i\);
- 设选择的关卡为 \(i\),贡献为 \(b_i-a_i\);
- 设第一次选择和第二次选择的关卡分别为 \(i,j\),贡献为 \(b_j-a_i\);
- 设第一次选择和第二次选择的关卡分别为 \(i,j\),贡献为 \(a_i-b_i+a_j\);
分析上面出现的内容,我们需要维护什么?
为了方便,我们需要维护的东西尽量少。分析发现我们只需要维护 \(a_i,-a_i,a_i-b_i,b_i-a_i,b_i\) 就能覆盖掉上面的所有情况。
然后好像写起来还很麻烦而且这是 CF 数据,所以给个代码咯。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct node{
LL val,pos;
node(){val=pos=0;}
node(LL P,LL V){val=V,pos=P;}
bool operator < (node another) const {return val>another.val;}
};
priority_queue<node> Q[6];
/*
拓展一颗星的方案有什么?
- 没选过->选一个 代价ai
- 选一个->选两个 代价bi-ai
- 选一个,另外一个不选->不选,另外一个选两个 代价bj-ai
- 选两个,另外一个不选->第一个选一个,另外一个选两个 ai-bi+bj
所以用 5 个堆维护
Q1:ai ->0
Q2:-ai ->1
Q3:ai-bi ->2
Q4:bi-ai ->1
Q5:bi ->0
*/
LL n,w,a[300005],b[300005],opt[300005];
void checkQueue(LL num,LL op){while(!Q[num].empty() && opt[Q[num].top().pos]!=op) Q[num].pop();}
int main(){
cin>>n>>w;
for(LL i=1;i<=n;++i) cin>>a[i]>>b[i],Q[1].push(node(i,a[i])),Q[5].push(node(i,b[i]));
LL ans=0;
for(LL i=1;i<=w;++i)
{
LL delta=2147483647,op=0;
node tmp1=node(),tmp2=node();
checkQueue(1,0);
checkQueue(2,1);
checkQueue(3,2);
checkQueue(4,1);
checkQueue(5,0);
if(!Q[1].empty() && Q[1].top().val<delta)
{
tmp1=Q[1].top();
op=1;
delta=Q[1].top().val;
}
if(!Q[4].empty() && Q[4].top().val<delta)
{
tmp1=Q[4].top();
op=2;
delta=Q[4].top().val;
}
if(!Q[2].empty() && !Q[5].empty() && Q[2].top().val+Q[5].top().val<delta)
{
tmp1=Q[2].top();
tmp2=Q[5].top();
op=3;
delta=Q[2].top().val+Q[5].top().val;
}
checkQueue(5,0);
if(!Q[3].empty() && !Q[5].empty() && Q[3].top().val+Q[5].top().val<delta)
{
tmp1=Q[3].top();
tmp2=Q[5].top();
op=4;
delta=Q[3].top().val+Q[5].top().val;
}
ans+=delta;
if(op==1)
{
opt[tmp1.pos]=1;
Q[2].push(node(tmp1.pos,-a[tmp1.pos]));
Q[4].push(node(tmp1.pos,b[tmp1.pos]-a[tmp1.pos]));
}
if(op==2)
{
opt[tmp1.pos]=2;
Q[3].push(node(tmp1.pos,a[tmp1.pos]-b[tmp1.pos]));
}
if(op==3)
{
opt[tmp1.pos]=0,opt[tmp2.pos]=2;
Q[1].push(node(tmp1.pos,a[tmp1.pos]));
Q[3].push(node(tmp2.pos,a[tmp2.pos]-b[tmp2.pos]));
Q[5].push(node(tmp1.pos,b[tmp1.pos]));
}
if(op==4)
{
opt[tmp1.pos]=1,opt[tmp2.pos]=2;
Q[2].push(node(tmp1.pos,-a[tmp1.pos]));
Q[3].push(node(tmp2.pos,a[tmp2.pos]-b[tmp2.pos]));
Q[4].push(node(tmp1.pos,b[tmp1.pos]-a[tmp1.pos]));
}
}
cout<<ans<<endl;
for(LL i=1;i<=n;++i) cout<<opt[i];
return 0;
}