[学习笔记] 反悔贪心
总结
这东西直接刷题吧。根据我做过的题有下列几个方法:
- 先乱贪心,然后设计反悔机制来修正答案。
- 先建出网络流模型,然后研究性质(凸凹性)
- 先建出费用流模型,然后模拟费用流(网络流的本质也是反悔贪心)
这东西和网络流关系密切,很多时候要结合着用。
UPD2021/7/17:今天 \(\tt cf\) 的一道反悔贪心把我干傻了,我补充一下如何判断设计的反悔贪心是否正确。
种树
题目描述
解法
据花姐姐说可以直接 \(wqs\) 二分,这种时候直接感性理解就好(这不是重点)
我们首先考虑一种乱贪心,也就是直接拿当前权值最大的,然后删去相邻的就可以了。
考虑这种贪心为什么会错,就是因为我们不一定选最大的,而可能同时选他相邻的两个。
反悔贪心的思想就是,我们先按照乱贪心的方法做,然后通过反悔来修正答案,这就需要我们根据乱贪心出现的问题来设计反悔机制。比如这道题,我们可以设计反悔机制为:再选一次当前这个树,代表反悔之前的选择,而选择旁边的两个树(其实就是等效为一棵树再塞回优先队列中)。
现在维护这个反悔机制就行了,我们可以维护一个双向链表提供每个位置两边的数,然后每次就直接优先队列找权值最大的位置,找到之后删除旁边的两个数,再更新这个点 \(t\) 的权值为 \(a[l[t]]+a[r[t]]-a[t]\),更新一下双端队列后把这个数放回优先队列就行了。
#include <cstdio>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 500005;
#define db double
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,ans,a[M],l[M],r[M],vis[M];
struct node
{
int x,c;
bool operator < (const node &b) const
{
return c<b.c;
}
};priority_queue<node> q;
int main()
{
n=read();k=read();
if(n<k*2)
{
puts("Error!");
return 0;
}
for(int i=1;i<=n;i++)
{
a[i]=read();
q.push(node{i,a[i]});
l[i]=i-1;r[i]=i+1;
}
l[1]=n;r[n]=1;
while(!q.empty())
{
int t=q.top().x;q.pop();
if(vis[t]) continue;
vis[l[t]]=vis[r[t]]=1;
ans+=a[t];
a[t]=a[l[t]]+a[r[t]]-a[t];
k--;
if(k==0) break;
//更新双向链表
l[t]=l[l[t]];
r[t]=r[r[t]];
r[l[t]]=t;
l[r[t]]=t;
q.push(node{t,a[t]});
}
printf("%d\n",ans);
}
建筑抢修
题目描述
解法
典型的反悔贪心。考虑乱贪心就是按 \(T_2\) 排序,每次就按着结束时间修建筑,能修就修。
会出的问题就是可以结束时间早的会占用很多时间,把后面的时间挤压了。那么我们考虑每次替换成时间消耗更小的,也就是维护一个优先队列考虑替换队首就行了。
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 150005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,sum;priority_queue<int> q;
struct node
{
int x,y;
bool operator < (const node &b) const
{
return y<b.y;
}
}a[M];
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),y=read();
a[i]=node{x,y};
}
sort(a+1,a+1+n);
for(int i=1;i<=n;i++)
{
int x=a[i].x;
if(sum+x<=a[i].y)//可以直接修
{
q.push(x);
sum+=x;
}
else if(x<q.top())
{
sum-=q.top();
q.pop();
q.push(x);
sum+=x;
}
}
printf("%d\n",q.size());
}
CF802O April Fools' Problem
题目描述
解法
这个题真的是个妙妙题。
首先不难建出网络流模型,源点向 \(a_i\) 连边,\(a_i\) 向 \(b_i\) 连边,\(b_i\) 向汇点连边,相邻两个 \(a_i,a_{i+1}\) 连边。但是因为数据规模太大所以说无脑费用流是跑不动的。
看到 恰好选k个
类似的字眼就想一想 \(\tt wqs\) 二分吧,这道题打印 \(k\) 道题就相当于恰好选 \(k\) 个嘛。但是 \(\tt wqs\) 还需要有凸单调性,从网络流的角度考虑,每次的最短路都在增加,也就是增量是递增的,那么 \(\tt wqs\) 二分就没问题了。
我们考虑在 \(b_i\) 上面加权,现在问题变成了无限制 \(a,b\) 匹配的最小代价,这个可以直接用反悔贪心实现,也就是我们在 \(b_i\) 和前面最小的 \(a\) 匹配之后也丢进优先队列,在后面的选择中可以反悔。
#include <cstdio>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,res,num,a[M],b[M];
struct node
{
int c,x;
bool operator < (const node &b) const
{
return c>b.c;
}
};
void work(int mid)
{
priority_queue<node> q;
res=num=0;
for(int i=1;i<=n;i++)
{
int x=b[i]-mid;
q.push(node{a[i],1});
node t=q.top();
if(x+t.c<=0)
{
q.pop();//取了再弹
res+=x+t.c;
num+=t.x;
q.push(node{-x,0});//供反悔
}
}
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=1;i<=n;i++)
b[i]=read();
int l=0,r=2e9;
while(l<=r)
{
int mid=(l+r)>>1;
work(mid);
if(num>=m)
{
ans=res+mid*m;
r=mid-1;
}
else l=mid+1;
}
printf("%lld\n",ans);
}
CF436E Cardboard Box
题目描述
解法
虽然这道题没有用反悔贪心我还是把他加上来了。
先模一下 Ark 巨佬,这个方法是真的顶。
先把所有 \(b_i\) 减去 \(a_i\) 得到新的 \(b_i\),也就是拿第二颗星需要多付出的代价(下文 \(b_i\) 都是这意思)
考虑将物品分类,第一类 \(a_i<b_i\),第二列 \(a_i\geq b_i\),可以将两类物品分别求解再合并答案。
对于第一类物品,因为一定会先取 \(a_i\) 再取 \(b_i\),所以直接把所有东西混在一起排序即可,记 \(g(x)\) 表示选了 \(x\) 颗星星的最大价值,那么一定是选取排序后数组的一个前缀。
对于第二类物品,因为此时选取 \(a_i\) 的目的是选取对应的 \(b_i\),那么不难导出一个结论:可以一组一组地选物品,至多只有一组只选了一个 \(a_i\),那么做法就呼之欲出了,我们把每组按 \(a_i+b_i\) 排序,记 \(f(x)\) 表示选了 \(x\) 星星的最大价值。那么 \(f(2x)\) 就直接拿前 \(i\) 组即可,\(f(2x+1)\) 就拿前 \(i\) 组再加上后面的 \(a_i\),或者是拿前 \(i+1\) 组再除去一个前面的 \(b_i\)
因为只用算一个位置的值,所以直接枚举第一类物品选的星星个数即可,时间复杂度 \(O(n\log n)\)
还有一个加强版,如果要算 \(\forall w\in[1,2n]\) 的最优解的话,因为 \(g(x)\) 是个凸函数,所以可以用决策单调性优化 \(\max\) 卷积,那么可以做到 \(O(n\log n)\) 啦!
还有这道题的结论是真的强,我已经是第二次见到了,是优化背包的经典结论吧!
\(\tt luogu\) 上说要算的是选取星数大于等于 \(w\) 的最优解,那你再把 \(w+1\) 算一次不就行了么?
下面写了一个暴力反悔贪心的代码....
#include <cstdio>
#include <cassert>
#include <iostream>
#include <queue>
using namespace std;
const int M = 300005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M],b[M],vis[M];
struct n1
{
int x;
n1(int X=0) : x(X) {}
bool operator < (const n1 &r) const
{
return a[x]>a[r.x];
}
};
struct n2
{
int x;
n2(int X=0) : x(X) {}
bool operator < (const n2 &r) const
{
return b[x]>b[r.x];
}
};
struct n3
{
int x;
n3(int X=0) : x(X) {}
bool operator < (const n3 &r) const
{
return a[x]+b[x]>a[r.x]+b[r.x];
}
};
struct n4
{
int x;
n4(int X=0) : x(X) {}
bool operator < (const n4 &r) const
{
return a[x]<a[r.x];
}
};
struct n5
{
int x;
n5(int X=0) : x(X) {}
bool operator < (const n5 &r) const
{
return b[x]<b[r.x];
}
};
signed main()
{
n=read();m=read();
priority_queue<n1> q1;
priority_queue<n2> q2;
priority_queue<n3> q3;
priority_queue<n4> q4;
priority_queue<n5> q5;
a[0]=b[0]=1e12;
for(int i=1;i<=n;i++)
{
a[i]=read();b[i]=read()-a[i];
q1.push(n1(i));
q3.push(n3(i));
}
while(m>0)
{
while(!q1.empty() && vis[q1.top().x]!=0) q1.pop();
while(!q2.empty() && vis[q2.top().x]!=1) q2.pop();
while(!q3.empty() && vis[q3.top().x]!=0) q3.pop();
while(!q4.empty() && vis[q4.top().x]!=1) q4.pop();
while(!q5.empty() && vis[q5.top().x]!=2) q5.pop();
//
int x=q1.empty()?0:q1.top().x;
int y=q2.empty()?0:q2.top().x;
int z=q3.empty()?0:q3.top().x;
int xx=q4.empty()?0:q4.top().x;
int yy=q5.empty()?0:q5.top().x;
int c1=a[x],c2=b[y],c3=a[z]+b[z],f=0;
//
if(xx) c3-=a[xx],f=1;
if(yy && a[z]+b[z]-b[yy]<=c3)
c3=a[z]+b[z]-b[yy],f=2;
int mi=min(c1,min(c2,c3));
//
m--;ans+=mi;
if(mi==c1)//a
{
assert(mi==a[x]);
vis[x]=1;
q2.push(n2(x));
q4.push(n4(x));
}
else if(mi==c2)//b
{
assert(mi==b[y]);
vis[y]=2;
q5.push(n5(y));
}
else//a+b
{
vis[z]=2;
if(f==1) assert(mi==a[z]+b[z]-a[xx]);
if(f==2) assert(mi==a[z]+b[z]-b[yy]);
assert(f>0);
if(f==1)
vis[xx]=0,q1.push(n1(xx)),q3.push(n3(xx));
if(f==2)
vis[yy]=1,q2.push(n2(yy)),q4.push(n4(yy));
}
}
//for(int i=1;i<=n;i++)
// if(vis[i]==1) ans+=a[i];
// else if(vis[i]==2) ans+=a[i]+b[i];
printf("%lld\n",ans);
for(int i=1;i<=n;i++)
printf("%lld",vis[i]);
}
[NOI2019] 序列
题目描述
解法
首先建出费用流模型(说实话有点难建),至少 \(L\) 个下标相同等价于至多 \(k-L\) 个下标不同,我们把选数看成一对一对地选,那么就等价于有 \(k-L\) 次机会不选下标相同的数,所以可以得到下图:
不难发现对上图跑最大费用最大流就能得到答案,但是 \(n\leq 2\cdot 10^5\) 显然是跑不动网络流的。
可以考虑模拟费用流,就是把费用流手玩出来嘛。那就要仔细研究研究这个图是怎么跑费用流的,首先如果红边有空余的流量,那么可以直接给红边流一点流量,因为经由他来流一定是最优的,这就相当于选一对未匹配 \(A_i+B_j\) 的最大。
我们还可以直接流一点黑边,这就相当于选一对未匹配的 \(A_i+B_i\) 最大。
最后一种方法就是使用费用流的返回边了,可以回撤某个 \(A_i\)(设其原来匹配的是 \(B_k\))到红边的流量,让他和 \(B_i\) 匹配,但是为了红边的流量平衡我们还需要补一个未匹配 \(A_j\) 的最大值上去,相当于我们匹配 \(A_j\) 和 \(B_k\)
还可以回撤某个 \(B_i\),这个和回撤 \(A_i\) 的方法是一样的。
思路大概就是这样,最后讲一下实现的细节(这部分一定要认真看,不然会卡很久):
-
我们要维护 \(5\) 个堆,分别是:未匹配 \(A\) 的最大值 \(h1\);\(B\) 匹配但 \(A\) 未匹配的 \(A\) 的最大值 \(f1\);未匹配 \(B\) 的最大值 \(h2\);\(A\) 匹配但是 \(B\) 未匹配的 \(B\) 的最大值 \(f2\);\(A,B\) 都未匹配的 \(A+B\) 最大值 \(h3\)
-
模拟的时候就一点一点的加入流量,\(4\) 种流法混在一起写就行了。写的时候会涉及到很繁琐的出堆入堆,方便的写法是维护一个标记数组 \(s\),我们不急着弹堆,而是在取出堆顶的时候看他合不合法即可,不合法就弹出去。
-
注意如果有下标相同的情况,就算是任意选择的,也不需要用红边了,一定要注意。
虽然没有注释,但是代码还是很好看的。
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
#define ll long long
const int M = 200005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,k,l,now,a[M],b[M],s[M];ll ans;
struct n1
{
int x;
n1(int X=0) : x(X) {}
bool operator < (const n1 &r) const
{
return a[x]<a[r.x];
}
};priority_queue<n1> f1,h1;
struct n2
{
int x;
n2(int X=0) : x(X) {}
bool operator < (const n2 &r) const
{
return b[x]<b[r.x];
}
};priority_queue<n2> f2,h2;
struct n3
{
int x;
n3(int X=0) : x(X) {}
bool operator < (const n3 &r) const
{
return a[x]+b[x]<a[r.x]+b[r.x];
}
};priority_queue<n3> h3;
void fuck()
{
while(!h1.empty()) h1.pop();
while(!f1.empty()) f1.pop();
while(!h2.empty()) h2.pop();
while(!f2.empty()) f2.pop();
while(!h3.empty()) h3.pop();
for(int i=1;i<=n;i++)
{
s[i]=0;
h1.push(n1(i));
h2.push(n2(i));
h3.push(n3(i));
}
while(k--)
{
while(!h1.empty() && (s[h1.top().x]&1)) h1.pop();
while(!f1.empty() && (s[f1.top().x]^2)) f1.pop();
while(!h2.empty() && (s[h2.top().x]&2)) h2.pop();
while(!f2.empty() && (s[f2.top().x]^1)) f2.pop();
while(!h3.empty() && s[h3.top().x]) h3.pop();
if(now)
{
now--;
int x=h1.top().x,y=h2.top().x;
ans+=a[x]+b[y];
s[x]|=1;s[y]|=2;
if(s[x]^3) f2.push(n2(x));
if(s[y]^3) f1.push(n1(y));
if(x==y) now++;
else
{
if(s[x]==3) now++;
if(s[y]==3) now++;
}
continue;
}
int v1=0,v2=0,v3=0,c1=0,c2=0;
if(!f2.empty())
{
v1=a[h1.top().x]+b[f2.top().x];
c1=s[h1.top().x]==2?1:0;
}
if(!f1.empty())
{
v2=a[f1.top().x]+b[h2.top().x];
c2=s[h2.top().x]==1?1:0;
}
if(!h3.empty())
v3=a[h3.top().x]+b[h3.top().x];
int mx=max(v1,max(v2,v3));ans+=mx;
if(v1==mx && (v1>v2 || (v1==v2 && c1>=c2)))
{
int x=h1.top().x,y=f2.top().x;
s[x]|=1;s[y]|=2;
if(s[x]^3) f2.push(n2(x));
else now++;
}
else if(v2==mx)
{
int x=f1.top().x,y=h2.top().x;
s[x]|=1;s[y]|=2;
if(s[y]^3) f1.push(n1(y));
else now++;
}
else s[h3.top().x]=3;
}
}
signed main()
{
T=read();
while(T--)
{
n=read();k=read();l=read();
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=1;i<=n;i++)
b[i]=read();
now=k-l;ans=0;
fuck();
printf("%lld\n",ans);
}
}
楼房搭建
题目描述
这道题是校内模拟赛的题,没有 \(\tt source\)
有 \(n\) 个的楼房,初始时每个位置高度都是 \(0\),每次操作可以让相邻的两个楼房高度 \(+1\) 和 \(+2\)(可以是左边加 \(1\),也可是是右边加 \(1\)),问最少需要操作多少次,使得操作后第 \(i\) 个楼房的高度不小于 \(h_i\)
\(1\leq n,h_i\leq 10^6\)
解法
本题可以用单调队列优化 \(dp\) 做到 \(O(n\cdot h_i)\),但是无法优化,这里就不展开讲了。
先想一想我们是怎么乱贪心的,目的显然是让多加的高度最小,一种看起来比较合理的贪心是:当处理到建筑 \(i\) 的时候我们疯狂放 \(2\),然后建筑 \(i+1\) 就对应的放 \(1\)
问题也是显然的,如果出现下图的情况就会 \(\tt Wa\) 掉:
最右边那个楼房就会浪费很多,我们本应该对第一个楼房实行 <1,2>
操作来削减第二个楼房留给后面的高度,但是我们无脑做 <2,1>
导致了错误,解决方法是在保证第一个楼房高度不变的情况下,我们尽可能升高第二个楼房。
可以用反悔贪心来实现它,具体地,我们用两个 <1,2>
操作来反悔一个 <2,1>
操作,这样第二个楼房就会升高 \(3\) 的高度。
但是这样还是会出问题,你怎么知道什么时候反悔?如果我们无脑反悔的话可能出现第三个楼房极高,我们就不需要第二个楼房反悔得这么高的情况,那么我们现在从第三楼房的视角去看我们的反悔操作,现在保证第二个楼房的高度不变,那么可以用 <1,2>and<2,1>
或者是三个 <1,2>
把第三个楼房升高 \(3\) 或者 \(6\),也就是说反悔上一个建筑的反悔操作可以让这个楼房升高 \(3\) 或者 \(6\)
有了这两个理论之后我们就可以无脑做了,相当于是每次让当前建筑尽量高,如果这样不合适也没关系,因为相信后面能够反悔过来,那么 \(O(n)\) 扫一遍就可以解决问题。
实现的时候,"反悔反悔操作"和反悔操作放在一个变量存着即可,\(+6\) 可以看做两个 \(+3\)
下面我嫖了 oneindark 巨佬的代码:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
int main(){
int n = readint();
long long ans = 0;
int v = 0; // already built
int chance = 0; // how many repent is allowed
for(int i=1; i<=n; ++i){
int h = readint(); ans += h;
h -= v, v = 0; // what's to do
if(h <= 0){ // finished
chance = 0; ans += (-h);
continue; // waste -h
}
int x = min(h/3,chance); // how many +3 is applied
int y = (h-3*x)>>1; // how many <2,1> is applied
v = (((h-3*x)&1)<<1)+y; // if <1,2> is applied
chance = (x<<1)+y; // +3 here can be twice +3 there
}
ans += v; // build on virtual n+1
printf("%lld\n",ans/3);
return 0;
}
CF335F Buy One, Get One Free
这题我也没搞懂,别看我写的东西!
题目描述
还是难啊,我本来以为我能直接切,但 \(3000\) 分的题怎么会那么容易!
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M],b[M],c[M],tmp[M];
priority_queue<int,vector<int>,greater<int> > q;
signed main()
{
n=read();
for(int i=1;i<=n;i++)
a[i]=read(),ans+=a[i];
sort(a+1,a+1+n);
for(int i=1,j;i<=n;i=j)
{
j=i;b[++m]=a[i];
for(;a[i]==a[j];j++);
c[m]=j-i;
}
for(int i=m,sc=0;i>=1;i--)
{
int num=q.size(),tp=0;
int t=min(sc-2*num,c[i]),p=min(c[i],sc)-t;
for(int j=1;j<=t;j++)
tmp[++tp]=b[i];//dirctly greedy
for(int j=1;j<=p;j+=2)
{
int k=q.top();q.pop();
if(k<b[i])//regret previous REGRET
{
tmp[++tp]=b[i];
if(j<p) tmp[++tp]=b[i];
}
else//regret
{
tmp[++tp]=k;
if(j<p && 2*b[i]>k)
tmp[++tp]=2*b[i]-k;
}
}
for(int j=1;j<=tp;j++)
q.push(tmp[j]);
sc+=c[i];
}
while(!q.empty()) ans-=q.top(),q.pop();
printf("%lld\n",ans);
}