总结
这东西直接刷题吧。根据我做过的题有下列几个方法:
- 先乱贪心,然后设计反悔机制来修正答案。
- 先建出网络流模型,然后研究性质(凸凹性)
- 先建出费用流模型,然后模拟费用流(网络流的本质也是反悔贪心)
这东西和网络流关系密切,很多时候要结合着用。
UPD20217/7/17:今天 ( t 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
",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
",q.size());
}
CF802O April Fools' Problem
题目描述
解法
这个题真的是个妙妙题。
首先不难建出网络流模型,源点向 (a_i) 连边,(a_i) 向 (b_i) 连边,(b_i) 向汇点连边,相邻两个 (a_i,a_{i+1}) 连边。但是因为数据规模太大所以说无脑费用流是跑不动的。
看到 恰好选k个
类似的字眼就想一想 ( t wqs) 二分吧,这道题打印 (k) 道题就相当于恰好选 (k) 个嘛。但是 ( t wqs) 还需要有凸单调性,从网络流的角度考虑,每次的最短路都在增加,也就是增量是递增的,那么 ( t 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
",ans);
}
CF436E Cardboard Box
题目描述
解法
虽然这道题没有用反悔贪心我还是把他加上来了。
先模一下 Ark 巨佬,这个方法是真的顶。
先把所有 (b_i) 减去 (a_i) 得到新的 (b_i),也就是拿第二颗星需要多付出的代价(下文 (b_i) 都是这意思)
考虑将物品分类,第一类 (a_i<b_i),第二列 (a_igeq 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(nlog n))
还有一个加强版,如果要算 (forall win[1,2n]) 的最优解的话,因为 (g(x)) 是个凸函数,所以可以用决策单调性优化 (max) 卷积,那么可以做到 (O(nlog n)) 啦!
还有这道题的结论是真的强,我已经是第二次见到了,是优化背包的经典结论吧!
( t 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
",ans);
for(int i=1;i<=n;i++)
printf("%lld",vis[i]);
}
[NOI2019] 序列
题目描述
解法
首先建出费用流模型(说实话有点难建),至少 (L) 个下标相同等价于至多 (k-L) 个下标不同,我们把选数看成一对一对地选,那么就等价于有 (k-L) 次机会不选下标相同的数,所以可以得到下图:
不难发现对上图跑最大费用最大流就能得到答案,但是 (nleq 2cdot 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
",ans);
}
}
楼房搭建
题目描述
这道题是校内模拟赛的题,没有 ( t source)
有 (n) 个的楼房,初始时每个位置高度都是 (0),每次操作可以让相邻的两个楼房高度 (+1) 和 (+2)(可以是左边加 (1),也可是是右边加 (1)),问最少需要操作多少次,使得操作后第 (i) 个楼房的高度不小于 (h_i)
(1leq n,h_ileq 10^6)
解法
本题可以用单调队列优化 (dp) 做到 (O(ncdot h_i)),但是无法优化,这里就不展开讲了。
先想一想我们是怎么乱贪心的,目的显然是让多加的高度最小,一种看起来比较合理的贪心是:当处理到建筑 (i) 的时候我们疯狂放 (2),然后建筑 (i+1) 就对应的放 (1)
问题也是显然的,如果出现下图的情况就会 ( t 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
",ans/3);
return 0;
}
CF335F Buy One, Get One Free
这题我也没搞懂,别看我写的东西!
题目描述
还是难啊,我本来以为我能直接切,但 (3000) 分的题怎么会那么容易!
错解1
首先考虑如果免费得到的是一个价格不严格小于的馅饼,那么就是一道水题,直接从大到小贪心即可。
这道题直接贪心不行,就反悔啊。我们考虑把所有免费的馅饼放进单调栈里面,对于每个馅饼带走一个严格比他小的,如果带不走那么就考虑反悔,买栈顶那个免费的馅饼,就能多带走当前的两个馅饼,判断条件是 (free<2cdot a_i) 时反悔。
样例随便过,但是一交上去就 ( t Wa) 穿了。
错解2
认真分析了一波,到处去翻题解。我发现不能带走严格比他小的馅饼啊,因为原本的贪心就不是这样设计的,原本的贪心是带走相邻的一个,不能这么乱做的。
我写不动,脑子炸了,留坑待填。
#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
",ans);
}