【小结】反悔贪心
定义
反悔贪心是当问题的决策很少时(修改的花费比较少时),而大的子问题不一定是由子问题的最优解得到,可以建立某种反悔的机制,从而在之后修正以前的错误决策的一种贪心。
我的理解是:反悔贪心相当于一维 DP,它用 \(dp_i\) 表示前 \(i\) 个数的最优决策。只是这个东西根据题目的一些性质无法保证是从 \(i-1\) 推到 \(i\) 就是最优的,所以就产生了“反悔”。反悔大多是以优先队列的方式来实现的。
反悔贪心是通过保证每一个子问题都是最优的从而通过某些特殊的撤销操作来实现从 \(dp_{i-1}\) 到 \(dp_i\) 的转移成功。
具体的,若 \(i-1\) 保证了最优方案,那么通过这种方式转移到 \(i\) 一定也是最优的。
我的理解是优先队列优化 DP。
我们需要找出这种方案,从而修正我们的“假贪心”。
反悔自动机是不用做出某些贪心决策,随便选都能修正过来的一种较高级的反悔贪心。
反悔堆则是需要一些贪心策略保证,才能使得每一个子问题最优的反悔贪心。
例题
CF865D Buy Low Sell High(反悔自动机)
反悔贪心模板题。
首先考虑每一天的决策数量都很少。所以考虑反悔贪心。
我在每一天前面都要目前的钱数最多。所以可以考虑假贪心。
每一次都使得在 \(i\) 点前面获得的钱数最多。所以到 $i $ 就卖。然后可以选择前面最便宜的一次买。
这显然是不对的。因为有可能不卖出,后面卖可能更优。
后面卖可能更优?那么考虑后面卖。
在一个点 \(i\) 买入,然后再 \(j\) 错误的卖出了,最后最优决策应该是 \(k\)。
那么此时我们已经计入了 \(p_j-p_i\) 的代价了。
要把它变为 \(p_k-p_i\) 的代价,就可以“反悔”,把代价加上 \(p_k-p_j\)。
这样,对于 \(k\),前面的东西有最小的,也有可以反悔的,在这之中,选择使得和的增量最大的,一次次放入。
Q: 如果这样,那 \(p_j\) 不就被操作了两次吗?
A: 这可以不操作。因为这是买入一次卖出一次。是废的。这只是撤销操作的等价需要。
总结:一种情况是卖出前面买的股票,另一种情况是修正前面的错误决策(也是在这一天卖掉)。最后,两种情况的式子是一样的是 \(p_k-p_j\),就形成了统一的式子。这就是通过特殊方式维护了最优子结构。
对于卖出了前面东西的 \(x\),要进入优先队列 \(2\) 次。
一次是为了有一种在 \(x\) 买入的情况,另一次是要为前面的修正做准备。
两者互不影响,如果我修正了 \(x\),那么也就是从 \(x\) 前面的某一个点买入,然后从 \(i\) 卖出。并不影响 \(x\) 可以是买入状态。
然后大根堆维护每一次的最大增量。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long long ll;
const int N=3e5+10;
int n,i,x;
ll ans;
priority_queue<int,vector<int>,greater<int> > q;
int main()
{
scan>>n;
for (i=1;i<=n;i++)
{
scan>>x;
q.push(x);
if (x>q.top())
{
ans+=x-q.top();
q.pop();
q.push(x);
}
}
printf("%lld",ans);
return 0;
}
[USACO09OPEN] Work Scheduling G(反悔堆)
价值不定,时间一定的题目。
首先按截止时间排序。
因为我肯定是先做截止前的再做截止后的。
如果两者交换,那么后者满足了一定前者满足。
所以截止时间从前到后是正确的
考虑“假贪心”,保证每一次的价值和都最大。
那么首先能加入就直接加入。
如果不能加入,那么把以前的一个东西移除之后,时间与上一个的时间相同,又因为上一个已经加进来了,所以这一个一定能加进来。
所以我们只会删除一个值。显然,要保证当前的价值最大,我们需要“反悔”的就是前面价值最小的点,如果把那个点删掉,这个点加进去,价值增加了,那么就对当前有利。
对于每一步,都可以从 \(i-1\) 的最优通向 \(i\) 的最优。
所以是正确的。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long long ll;
const int N=1e5+10;
int n,i,cnt;
ll ans;
priority_queue<int,vector<int>,greater<int> > q;
struct Node{
int d,p;
}a[N];
bool cmp(Node x,Node y)
{
return x.d<y.d;
}
int main()
{
scan>>n;
for (i=1;i<=n;i++) scan>>a[i].d>>a[i].p;
sort(a+1,a+n+1,cmp);
for (i=1;i<=n;i++)
{
if (cnt<a[i].d)
{
cnt++;
ans+=a[i].p;
q.push(a[i].p);
}
else
{
if (!q.empty()&&a[i].p>q.top())
{
ans+=a[i].p-q.top();
q.pop();
q.push(a[i].p);
}
}
}
printf("%lld",ans);
return 0;
}
P4053 [JSOI2007] 建筑抢修(反悔堆)
价值一定,时间不定的题。
还是按截止时间从小到大排序(与上题同理)。
然后对于 \(i\) 前面,我需要最优,也就是单纯保证前面的价值最大。
如果当前可以直接放入,那么直接放入。
否则,只能最多剔除一个值,因为如果剔除至少两个值,那么加入这一个值,亏。
在同等条件下,时间越小越好。所以取出前面占用时间最大的,弄出来即可。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
#define int long long
const int N=2e5+10;
int n,i,last,ans;
priority_queue<int> q;
struct Node{
int len,t;
}a[N];
bool cmp(Node x,Node y)
{
return x.t<y.t;
}
signed main()
{
scan>>n;
for (i=1;i<=n;i++) scan>>a[i].len>>a[i].t;
sort(a+1,a+n+1,cmp);
for (i=1;i<=n;i++)
{
if (last+a[i].len<=a[i].t)
{
last+=a[i].len;
q.push(a[i].len);
ans++;
}
else if (!q.empty()&&a[i].len<q.top())
{
last=last-q.top()+a[i].len;
q.pop();
q.push(a[i].len);
}
}
printf("%lld",ans);
return 0;
}
P1484 种树(返回堆+双向链表优化反悔贪心)
典题。
考虑我们想要取最大值。
显然,这是错误的。
比如一组数据:
3 2
88 99 88
这种算法输出为 \(99\),但实际结果为 \(176\)。
所以得到启发:如果我们不选某一个当前的最大值,那么一定会选它的两边。而且一定两边都会选。
因为如果只选一边,那么另一边也没选,选旁边的一个既影响了旁边的点,又使得和更小。而移动到最大值,既不影响其他点,又和更大。
所以考虑每一次就先选最大值。然后反悔。
反悔的做法是选两边,不选中间。
此时与两边相邻的点也不能选了,我们把这个点两边标记。再用反悔的增量 \(a_{i-1}+a_{i+1}-a_i\) 赋值 \(a_i\)。
注意,此时 \(a_i\) 两边不能再选,因为除了反悔的情况不能选两边,而反悔的情况已经被 \(a_i\) 考虑。
此时把 \(a_i\) 入队。如果反悔,那么此时又有一个问题。
此时相邻的点变了。那么考虑双向链表维护两边的点,然后就可以快速查询了。
会了查询之后就一直合并,因为一直可以反悔。每一次都有价值。
把这个过程进行 \(k\) 次,因为是至多 \(k\) 次,所以价值小于等于 \(0\) 的时候可以不计入,直接终止。
这是这种题的根本。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long long ll;
typedef pair<ll,int> pii;
const int N=3e5+10;
int n,k,i,id;
ll ans,x;
bool vis[N];
priority_queue<pii> q;
struct Node{
int l,r,val;
}a[N];
void del(int x)
{
a[x].l=a[a[x].l].l;
a[x].r=a[a[x].r].r;
a[a[x].l].r=x;
a[a[x].r].l=x;
}
int main()
{
scan>>n>>k;
for (i=1;i<=n;i++)
{
scan>>a[i].val;
a[i].l=i-1;
a[i].r=i+1;
q.push({a[i].val,i});
}
for (i=1;i<=k;i++)
{
while (!q.empty()&&vis[q.top().second]) q.pop();
x=q.top().first;
id=q.top().second;
q.pop();
if (x<=0) break;
ans+=x;
a[id].val=a[a[id].l].val+a[a[id].r].val-a[id].val;
vis[a[id].l]=vis[a[id].r]=true;
q.push({a[id].val,id});
del(id);
}
printf("%lld",ans);
return 0;
}
P1792 [国家集训队] 种树(基本是双倍经验)
发现与上一题差不多,但是有 \(3\) 点不同:
-
题目变成了环,可以令 \(1\) 左边为 \(n\),\(n\) 右边为 \(1\)。
-
有无解的情况,需要判断无解。
-
恰好 \(k\) 个,所以不能当价值小于等于 \(0\) 时终止。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long long ll;
typedef pair<int,int> pii;
const int N=3e5+10;
int n,k,i,p,id;
priority_queue<pii> q;
ll ans;
pii tp;
bool vis[N];
struct Node{
int l,r,val;
}a[N];
void del(int x)
{
a[x].l=a[a[x].l].l;
a[x].r=a[a[x].r].r;
a[a[x].l].r=x;
a[a[x].r].l=x;
}
int main()
{
scan>>n>>k;
for (i=1;i<=n;i++)
{
scan>>a[i].val;
if (i!=1) a[i].l=i-1;
else a[i].l=n;
if (i!=n) a[i].r=i+1;
else a[i].r=1;
q.push({a[i].val,i});
}
if (k>n/2)
{
puts("Error!");
return 0;
}
for (i=1;i<=k;i++)
{
while (!q.empty()&&vis[q.top().second]) q.pop();
tp=q.top();
q.pop();
p=tp.first;
id=tp.second;
ans+=p;
vis[a[id].l]=vis[a[id].r]=true;
a[id].val=a[a[id].l].val+a[a[id].r].val-a[id].val;
q.push({a[id].val,id});
del(id);
}
printf("%lld",ans);
return 0;
}
P3620 [APIO/CTSC2007] 数据备份(基本是三倍经验)
容易发现一定是相邻的在连。
所以直接差分,然后就转化为了种树。
需要注意的点:
-
恰好 \(K\) 个
-
最小长度。注意边界情况,如果有一边是没有的,那么这种情况增加不了选择数量的值。
-
值一定要更新。不能在判断中更新。
-
两边赋值为 \(inf\)。以前是最大值所以赋值 \(0\) 没关系。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long ll;
typedef pair<int,int> pii;
const int N=1e5+10;
const ll inf=1e16;
int n,k,i,p,id,pos[N];
priority_queue<pii,vector<pii>,greater<pii> > q;
ll ans;
pii tp;
bool vis[N];
struct Node{
int l,r;
ll val;
}a[N];
inline void del(int x)
{
a[x].l=a[a[x].l].l;
a[x].r=a[a[x].r].r;
a[a[x].l].r=x;
a[a[x].r].l=x;
}
int main()
{
scan>>n>>k;
for (i=1;i<=n;i++) scan>>pos[i];
for (i=1;i<n;i++) a[i].val=pos[i+1]-pos[i];
n--;
for (i=1;i<=n;i++)
{
a[i].l=i-1;
a[i].r=i+1;
q.push({a[i].val,i});
}
a[0].val=a[n+1].val=inf;
for (i=1;i<=k;i++)
{
while (!q.empty()&&vis[q.top().second]) q.pop();
tp=q.top();
q.pop();
p=tp.first;
id=tp.second;
ans+=p;
vis[a[id].l]=vis[a[id].r]=true;
a[id].val=a[a[id].l].val+a[a[id].r].val-a[id].val;
if (a[id].l>=1&&a[id].r<=n) q.push({a[id].val,id});
del(id);
}
printf("%lld",ans);
return 0;
}
P2672 [NOIP 2015 普及组] 推销员(反悔堆)
一句话题意:有一些 \(a_i\) 和 \(b_i\),你需要选取 \(X\) 个数,对于 \(X=1\dots n\),求 \(\max({\sum_{i=1}^{n}a_i+2\times\max{b_i}})\)
感觉不一定特别属于反悔贪心。
首先设计“假贪心”。
选取 \(a_i\) 最大的 \(X\) 个数。
然后考虑“反悔”。
结论:我们最多反悔一个数。
证明(反证):如果我反悔了大于等于 \(2\) 个数,那么我一定可以把距离更小的那个换成反悔的其中一个数。结果一定更大(因为 \(\max{b_i}\) 不变,\(\sum_{i=1}^{n} a_i\) 变大)。
所以考虑两种情况:
-
选取最大 \(X\) 个。可以排序。然后维护前缀 \(sum_i\) 和 \(dis_i\) 分别表示前缀 \(a_i\) 和,前缀 \(2\times b_i\) 最大值。就可以用 \(sum_i+dis_i\) 算。
-
替换一个。那么此时肯定替换更大距离的。而且肯定用 当前 \(a_i\) 最小的替换。因为对和的影响最小。所以最大距离被刷新。总的和就是 \(sum_{i-1}+a_j+2\times b_j\)。用 \(nxt_i\) 维护 \(i\) 以后得 \(a_j+2\times b_j\) 最大值即可。
就可以了。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef long long ll;
const int N=1e5+10;
int n,i,dis[N],nxt[N],sum[N];
struct Node{
int x,y;
}a[N];
bool cmp(Node o,Node p){return o.y>p.y;}
int main()
{
scan>>n;
for (i=1;i<=n;i++) scan>>a[i].x;
for (i=1;i<=n;i++) scan>>a[i].y;
sort(a+1,a+n+1,cmp);
for (i=1;i<=n;i++) sum[i]=sum[i-1]+a[i].y;
for (i=1;i<=n;i++) dis[i]=max(dis[i-1],2*a[i].x);
for (i=n;i>=1;i--) nxt[i]=max(nxt[i+1],2*a[i].x+a[i].y);
for (i=1;i<=n;i++) printf("%d\n",max(sum[i]+dis[i],sum[i-1]+nxt[i+1]));
return 0;
}
CF730I Olympiad in Programming and Sports(反悔堆)
首先只考虑编程团队。直接选编程能力最大的 \(p\) 个人然后标记为 \(1\)。
然后考虑反悔。我们可以一个一个的加入运动团队的人。
枚举 \(i\) 表示目前运动团队有多少人。然后考虑一个个加入。前提是保证编程团队人数不变。
于是有 \(2\) 种选择:
-
直接放入一个目前没有进入任何一个团队的最大的 \(b_i\)。
-
把一个编程团队的人放到运动团队中,然后选一个没标记的编程能力最大的人替补。
这就是加入 \(b_i\) 的两种方法。
于是维护 \(3\) 个队列 \(q1,q2,q3\)。
\(q1\) 维护 \(a_i\) 从大到小。\(q2\) 维护 \(b_i\) 从大到小。\(q3\) 维护可以反悔的增量从大到小。
于是每一次都把 \(q1,q2\) 中有标记的踢掉,然后在编程团队的人就 push 进 \(q3\) 里。每一次判断两种选择即可。
注意:必须要 \(q1\) 和 \(q3\) 同时非空才能和 \(q2\) 比较。因为要保证替补之后人数不变。
\(q3\) 从头到尾一定是有 \(p\) 个元素的。
还有,替补的 \(q1\) 一定要 push 进 \(q3\)。
然后就没有了。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
typedef pair<int,int> pii;
const int N=3e3+10;
int n,i,p,s,x,y,ans;
int vis[N];
priority_queue<pii> q1,q2,q3;
struct Node{
int x,y,id;
}a[N];
bool cmp(Node o,Node p){return o.x>p.x;}
void work()
{
while (!q1.empty()&&vis[q1.top().second]) q1.pop();
while (!q2.empty()&&vis[q2.top().second]) q2.pop();
if (!q2.empty())
{
if (q1.empty()||q3.empty())
{
ans+=q2.top().first;
vis[q2.top().second]=2;
}
else
{
if (q2.top().first>=q1.top().first+q3.top().first)
{
ans+=q2.top().first;
vis[q2.top().second]=2;
}
else
{
ans+=q1.top().first+q3.top().first;
vis[q1.top().second]=1;
vis[q3.top().second]=2;
q3.push({a[q1.top().second].y-a[q1.top().second].x,q1.top().second});
q3.pop();
}
}
}
else
{
ans+=q1.top().first+q3.top().first;
vis[q1.top().second]=1;
vis[q3.top().second]=2;
q3.push({a[q1.top().second].y-a[q1.top().second].x,q1.top().second});
q3.pop();
}
}
int main()
{
scan>>n>>p>>s;
for (i=1;i<=n;i++) scan>>a[i].x;
for (i=1;i<=n;i++) scan>>a[i].y;
for (i=1;i<=n;i++) a[i].id=i;
sort(a+1,a+n+1,cmp);
for (i=1;i<=p;i++)
{
q3.push({a[i].y-a[i].x,i});
ans+=a[i].x;
vis[i]=1;
}
for (i=1;i<=n;i++)
{
q1.push({a[i].x,i});
q2.push({a[i].y,i});
}
for (i=1;i<=s;i++) work();
printf("%d\n",ans);
for (i=1;i<=n;i++)
{
if (vis[i]==1) printf("%d ",a[i].id);
}
printf("\n");
for (i=1;i<=n;i++)
{
if (vis[i]==2) printf("%d ",a[i].id);
}
return 0;
}

浙公网安备 33010602011771号