模拟费用流学习笔记
费用流可以通过建图求解很多最优化一类的问题,但是它的时间复杂度较低
在费用流的过程中,每次增广出一条边来是的流量+1,可以等价于选择了一种决策。
和普通的贪心不一样的是,费用流每次增广所做出的决策可以是撤销之前做出的某个决策并且加入一个新的决策。
在一些情况下我们可以用其他一些方法去模拟费用流进行的一些操作,其实就是我们经常说的反悔贪心。
有一类经典的模拟费用流的问题是以老鼠进洞为模型来做的。
基础问题
在坐标轴上你有\(n\)只老鼠,\(m\)个洞,老鼠只能往左边走,一个洞最多进一只老鼠,进洞的代价就是老鼠走的路程,问让所有老鼠进洞最小的代价是多少?
这个问题非常的简单,从左到右扫描,遇到一个老鼠就把它放进最靠右的没有老鼠的洞里,这个东西可以用一个数组维护出来。
如果问最大代价呢?
Buy Low, Buy Lower
给定每天股票的价格,每天可以买入一股股票/卖出一股股票/闲着。问可以获得的最大收益。
假设我们要在今天卖出一股股票,那么相当于我们要和之前可能会买入的股票进行配对,代价就是一个类似于\(a_i-a_j\)一样的东西,那么我们可以用一个堆来维护\(-a_j\)这个东西,每次取堆顶进行匹配。
但是我们要最大化收益,我们这个东西不一定要一定卖,所以我们将\(-a_i\)再次当做一股股票放入堆中,如果后面匹配到这个位置,那么代价便是\(a_k-a_i+a_i-a_j=a_k-a_j\)刚好就是\(i\)这个位置被消掉的代价。
变形1:每个洞有额外代价
对于每个洞,打开它会有一个额外的代价\(b_i\),老鼠不必要全部进洞,求最大化代价。
仍然从左到右去扫描,我们对于所有洞,用堆来维护\(w_i-x_i\)这个值,和老鼠去匹配。
对于反悔堆的处理,这时由于开启洞的代价我们已经算进去了,所以只需要维护坐标的相反数就好了。
如果我们强制每个点都要匹配上呢?
在匹配的时候,如果只是最大化值,那么如果堆顶小于0就不去匹配了,如果要求每个点必须匹配,那么在老鼠来了的时候一定要去弹堆顶。
或者更简单一点,我们初始然每个老鼠直接去匹配一个代价负无穷小的洞,这样就强制让每只老鼠去匹配了。
bzoj4977跳伞求生
和原问题基本一样。
struct node{
int a,b;
inline bool operator <(const node &x)const{
if(a!=x.a)return a<x.a;
return b<x.b;
}
}a[N];
priority_queue<int>q;
int main(){
n=rd();m=rd();
for(int i=1;i<=n;++i){
a[i].a=rd();a[i].b=-1;
}
for(int i=1;i<=m;++i){
a[i+n].a=rd();a[i+n].b=rd();
}
sort(a+1,a+n+m+1);
ll ans=0;
for(int i=1;i<=n+m;++i){
if(a[i].b==-1){
if(!q.empty()){
ans+=a[i].a+q.top();
q.pop();q.push(-a[i].a);
}
}
else{
q.push(a[i].b-a[i].a);
}
}
cout<<ans<<endl;
return 0;
}
变形2 左右都可以走
这个问题就稍微复杂一些了,因为我们在完成一次老鼠进洞的匹配之后,老鼠和洞都可以被返回掉。
假设当前匹配的是\(b_i-a_j+w_j\),那么如果对老鼠进行退流,相当于这只老鼠再去匹配别的洞,就是将老鼠的坐标取反再减去这次匹配产生的价值(等价于费用流的反边)。如果对洞退流,相当于它再去匹配别的老鼠,那么可以直接把它当成老鼠去做,但是我们要对老鼠和洞分别开堆来维护答案。
变形3 一个洞可以进入多次
这个容易解决,在堆里再记录一下匹配个数,每次最多将一个元素分裂成两个,复杂度有保证。
【UER #8】雪灾与外卖
struct node{
ll a,b,c;
inline bool operator <(const node &x)const{
if(a!=x.a)return a<x.a;
return b<x.b;
}
}a[N];
struct nd{
ll a,b;
inline bool operator <(const nd &x)const{
if(a!=x.a)return a>x.a;
return b<x.b;
}
};
priority_queue<nd>q1,q2;
int main(){
n=rd();m=rd();
for(int i=1;i<=n;++i){
a[i].a=rd();a[i].b=-1;a[i].c=0;
}
ll sm=0;
for(int i=1;i<=m;++i){
a[i+n].a=rd();a[i+n].b=rd();a[i+n].c=rd();
sm+=a[i+n].c;
}
if(sm<n){
puts("-1");
return 0;
}
sort(a+1,a+n+m+1);
q1.push(nd{maxn,maxn});
ll ans=0;
for(int i=1;i<=n+m;++i){
if(a[i].b==-1){
nd now=q1.top();q1.pop();
now.b--;
ans+=a[i].a+now.a;
if(now.b)q1.push(now);
q2.push(nd{-now.a-2*a[i].a,1});
}
else{
ll sm=0;
while(!q2.empty()&&a[i].c){
nd now=q2.top();
if(now.a+a[i].b+a[i].a>0)break;
q2.pop();
ll num=min(a[i].c,now.b);
ans+=num*(now.a+a[i].b+a[i].a);
now.b-=num;a[i].c-=num;
sm+=num;
q1.push(nd{-now.a-2*a[i].a,num});
if(now.b)q2.push(nd{now.a,now.b});
}
if(sm)q2.push(nd{-a[i].a-a[i].b,sm});
if(a[i].c)q1.push(nd{a[i].b-a[i].a,a[i].c});
}
}
cout<<ans<<endl;
return 0;
}
变形3 树上问题
操作和上面一样,但是匹配变成了链上,在一个\(LCA\)的位置,我们要先去合并所有子树两两之间的答案,再去把剩下的元素合并起来往上传,这个可以用可并堆实现。
struct node{
ll val,num;
inline bool operator <(const node &b)const{
return val<b.val;
}
};
node val[N];
inline int newnode(ll x,ll y){
int now=top?st[top--]:++totnum;
val[now]=node{x,y};
d[now]=1;ch[now][0]=ch[now][1]=0;
return now;
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(val[y]<val[x])swap(x,y);
ch[x][1]=merge(ch[x][1],y);
if(d[ch[x][0]]<d[ch[x][1]])swap(ch[x][0],ch[x][1]);
d[x]=d[ch[x][1]]+1;
return x;
}
inline int pop(int x){
st[++top]=x;
return merge(ch[x][0],ch[x][1]);
}
void push(int &x,ll a,ll b){
int nw=newnode(a,b);
x=merge(x,nw);
}
struct edge{
int n,to,l;
}e[N<<1];
inline void add(int u,int v,int l){
e[++tot].n=head[u];
e[tot].to=v;
e[tot].l=l;
head[u]=tot;
}
inline void solve(int u,int v,ll dep){
while(T1[u]&&T2[v]){
node nw1=val[T1[u]],nw2=val[T2[v]];
if(nw1.val+nw2.val-dep*2>=0)break;
ll num=min(nw1.num,nw2.num);
val[T1[u]].num-=num;
val[T2[v]].num-=num;
ans+=(nw1.val+nw2.val-dep*2)*num;
if(!val[T1[u]].num)T1[u]=pop(T1[u]);
if(!val[T2[v]].num)T2[v]=pop(T2[v]);
push(T1[u],-nw2.val+dep*2,num);
push(T2[v],-nw1.val+dep*2,num);
}
}
void dfs(int u,int fa){
if(x[u])T1[u]=newnode(deep[u],x[u]);
if(y[u])T2[u]=newnode(deep[u]-inf,y[u]),ans+=inf*y[u];
for(int i=head[u];i;i=e[i].n)if(e[i].to!=fa){
int v=e[i].to;
deep[v]=deep[u]+e[i].l;
dfs(v,u);
solve(u,v,deep[u]);
solve(v,u,deep[u]);
T1[u]=merge(T1[u],T1[v]);
T2[u]=merge(T2[u],T2[v]);
}
}
int main(){
n=rd();
int u,v,w;
for(int i=1;i<n;++i){
u=rd();v=rd();w=rd();
add(u,v,w);add(v,u,w);
}
for(int i=1;i<=n;++i){
x[i]=rd(),y[i]=rd();
ll nw=min(x[i],y[i]);
x[i]-=nw;y[i]-=nw;
}
dfs(1,0);
cout<<ans<<endl;
return 0;
}
拓展应用
种树
给出一个环形结构,每个点种树有一个收益,相邻的两个位置不能同时种树,问最大收益。
首先我们肯定要先流最大的点,然后这个点流完之后相邻的位置就不能流了,那么我们把相邻两个位置删掉,那么如果这个位置我们反悔了退流了,那么相邻的两个位置是一定会被加回来的(如果只加回来一个肯定不优),所以说我们用两边位置的和减去当前位置作为新的元素插入进来继续贪心,直到流量为负数的时候退出。
CF436E
有\(n\)关,每关可以花\(a_i\)块钱获得一颗星,或者花\(b_i\)块钱获得两颗星,问获得\(w\)颗星的最小代价。
一个比较简单的做法是先按照\(b\)从小到大排序,然后枚举过两关的最后一个位置,那么它前面的每个位置都是至少选一个,然后给它选上,那么剩下的\(n-1\)个位置每个位置最多放一个了,可以用线段树来维护。
然后是贪心做法,考虑在当前形势下,我们增广出一条流量之后会发生什么。
\(1\)、增加一个一星卡。
\(2\)、将一个一星卡升级为二星卡。
\(3\)、将一个二星卡退成一星卡,再加入一张二星卡。
\(4\)、将一个一星卡退成零星卡,再加入一张二星卡。
那么我们可以用五个堆来维护出这里面的所有需要维护的信息,每次增广的时候\(check\)一下取价值最大的选。
可能会出现不合法的情况,我们对每个位置记录一下选择方案,堆顶不合法就弹出。
struct node{
ll val;int id;
inline bool operator <(const node &b)const{
return val>b.val;
}
};
priority_queue<node>q1,q2,q3,q4,q5;
inline void add0(int id){
q1.push(node{a[id],id});
q3.push(node{b[id],id});
}
inline void add1(int id){
q2.push(node{b[id]-a[id],id});
q4.push(node{-a[id],id});
}
inline void add2(int id){
q5.push(node{a[id]-b[id],id});
}
int main(){
n=rd();w=rd();
for(int i=1;i<=n;++i){
a[i]=rd();b[i]=rd();
add0(i);
}
ll ans=0;
while(w){
while(!q1.empty()&&tag[q1.top().id]!=0)q1.pop();
while(!q2.empty()&&tag[q2.top().id]!=1)q2.pop();
while(!q3.empty()&&tag[q3.top().id]!=0)q3.pop();
while(!q4.empty()&&tag[q4.top().id]!=1)q4.pop();
while(!q5.empty()&&tag[q5.top().id]!=2)q5.pop();
ll num=1e18;
if(!q1.empty())num=min(num,q1.top().val);
if(!q2.empty())num=min(num,q2.top().val);
if(!q3.empty()&&!q4.empty())num=min(num,q3.top().val+q4.top().val);
if(!q3.empty()&&!q5.empty())num=min(num,q3.top().val+q5.top().val);
if(!q1.empty()&&num==q1.top().val){
ans+=q1.top().val;int id=q1.top().id;
tag[id]=1;
q1.pop();
add1(id);
}
else
if(!q2.empty()&&num==q2.top().val){
ans+=q2.top().val;int id=q2.top().id;
tag[id]=2;
q2.pop();
add2(id);
}
else
if(!q3.empty()&&!q4.empty()&&q3.top().val+q4.top().val==num){
ans+=q3.top().val+q4.top().val;
tag[q3.top().id]=2;tag[q4.top().id]=0;
add2(q3.top().id);add0(q4.top().id);
q3.pop();q4.pop();
}
else{
ans+=q3.top().val+q5.top().val;
tag[q3.top().id]=2;tag[q5.top().id]=1;
add2(q3.top().id);add1(q5.top().id);
q3.pop();q5.pop();
}
w--;
}
cout<<ans<<endl;
for(int i=1;i<=n;++i)printf("%d",tag[i]);
return 0;
}
[NOI2019] 序列
不能说完全一样,只能说一模一样
有两个长度为\(n\)的序列,你要在两个序列中各选\(k\)个位置,但在两个序列当中都被选的位置至少要有\(L\)个,问最大代价。
我们先贪心,每个序列选\(k\)个最大的,这样可能选不够\(L\)个位置,假设还差\(num\)个,那么我们相当于是要增广\(num\)次,每次有以下几种决策。
\(1\)、去掉一个\(a\),把一个\(b\)换成\(ab\)
\(2\)、去掉一个\(b\),把一个\(a\)换成\(ab\)
\(3\)、去掉一个\(a\)和一个\(b\),把一个空换成\(ab\)
4、去掉一个\(ab\),把一个\(a\)和一个\(b\)换成两个\(ab\)
这样我们需要六个堆来维护这些信息,维护方法和上一题相同。
struct node{
ll val;int id;
inline bool operator <(const node &b)const{
return val<b.val;
}
}a[N],b[N];
inline bool cmp(node a,node b){
return a.val>b.val;
}
priority_queue<node>q1,q2,q3,q4,q5,q6;
inline void add0(int id){
q1.push(node{va[id]+vb[id],id});
}
inline void add1(int id){
q2.push(node{-va[id],id});
q3.push(node{vb[id],id});
}
inline void add2(int id){
q4.push(node{-vb[id],id});
q5.push(node{va[id],id});
}
inline void add3(int id){
q6.push(node{-va[id]-vb[id],id});
}
int main(){
int T=rd();
while(T--){
n=rd();k=rd();l=rd();
for(int i=1;i<=n;++i)a[i].val=rd(),a[i].id=i,va[i]=a[i].val,tag[i]=0;
for(int i=1;i<=n;++i)b[i].val=rd(),b[i].id=i,vb[i]=b[i].val;
sort(a+1,a+n+1,cmp);
sort(b+1,b+n+1,cmp);
ll ans=0;
for(int i=1;i<=k;++i){
ans+=a[i].val+b[i].val;
tag[a[i].id]++;
tag[b[i].id]+=2;
}
for(int i=1;i<=n;++i){
if(tag[i]==3)l--,add3(i);
if(tag[i]==2)add2(i);
if(tag[i]==1)add1(i);
if(tag[i]==0)add0(i);
}
while(l>0){
while(!q1.empty()&&tag[q1.top().id]!=0)q1.pop();
while(!q2.empty()&&tag[q2.top().id]!=1)q2.pop();
while(!q3.empty()&&tag[q3.top().id]!=1)q3.pop();
while(!q4.empty()&&tag[q4.top().id]!=2)q4.pop();
while(!q5.empty()&&tag[q5.top().id]!=2)q5.pop();
while(!q6.empty()&&tag[q6.top().id]!=3)q6.pop();
ll num=-1e18;
if(!q1.empty()&&!q2.empty()&&!q4.empty())
num=max(num,q1.top().val+q2.top().val+q4.top().val);
if(!q2.empty()&&!q5.empty()){
num=max(num,q2.top().val+q5.top().val);
}
if(!q3.empty()&&!q4.empty()){
num=max(num,q3.top().val+q4.top().val);
}
if(!q3.empty()&&!q5.empty()&&!q6.empty()){
num=max(num,q5.top().val+q6.top().val+q3.top().val);
}
if(!q1.empty()&&!q2.empty()&&!q4.empty()&&q1.top().val+q2.top().val+q4.top().val==num){
ans+=q1.top().val+q2.top().val+q4.top().val;
int id=0;
id=q1.top().id;q1.pop();tag[id]=3;add3(id);
id=q2.top().id;q2.pop();tag[id]=0;add0(id);
id=q4.top().id;q4.pop();tag[id]=0;add0(id);
}
else
if(!q2.empty()&&!q5.empty()&&q2.top().val+q5.top().val==num){
ans+=q2.top().val+q5.top().val;
int id=0;
id=q2.top().id;q2.pop();tag[id]=0;add0(id);
id=q5.top().id;q5.pop();tag[id]=3;add3(id);
}
else
if(!q3.empty()&&!q4.empty()&&q3.top().val+q4.top().val==num){
ans+=q3.top().val+q4.top().val;
int id=0;
id=q3.top().id;q3.pop();tag[id]=3;add3(id);
id=q4.top().id;q4.pop();tag[id]=0;add0(id);
}
else
if(!q3.empty()&&!q5.empty()&&!q6.empty()&&q3.top().val+q5.top().val+q6.top().val==num){
ans+=q3.top().val+q5.top().val+q6.top().val;
int id=0;
id=q3.top().id;q3.pop();tag[id]=3;add3(id);
id=q5.top().id;q5.pop();tag[id]=3;add3(id);
id=q6.top().id;q6.pop();tag[id]=0;add0(id);
}
l--;
}
printf("%lld\n",ans);
while(!q1.empty())q1.pop();
while(!q2.empty())q2.pop();
while(!q3.empty())q3.pop();
while(!q4.empty())q4.pop();
while(!q5.empty())q5.pop();
while(!q6.empty())q6.pop();
}
return 0;
}