模拟费用流有2种:第一种是考虑图的特殊性,使用合适的数据结构维护这个图。第二种是考虑这个费用流的实际意义,使用其他算法代替费用流。
先说第一种。模拟的有2种:最短增广路,消圈
增广路费用流算法的3条重要性质:
1.每次增广后,若流量为k,则现在的解是流量为k的最优解。
2.和源,汇点相邻的边不会被退流
3.如果不是每次选一条全局最短路进行增广,而是选择任意一条和源点相连的,以后必然会流的边进行增广(假设这条边指向a,且每次选择a~汇点的边进行增广),则也能得到最小费用最大流。
不会证明。
如果老鼠只能向右走,则显然可以构建图:
1.s->每个老鼠,费用0,流量1
2.离散化,每个老鼠/洞连向它右边的第一个关键点,费用距离,流量无限(老鼠或洞)
3.每个洞->t,费用0,流量1
从右往左扫,每次给老鼠分配当前最近且未被占用的洞,则发现这样子模拟了“每次取任意一条以后必然会流的边进行增广",由性质2,这样子是正确的。用一个栈维护,每次遇到洞插入进入栈,遇到老鼠弹出栈顶统计答案。
实际上,从右->左增广的好处是不用考虑反向边。
ctsc2010 产品销售运用到了这些性质。
显然可以构建图:
1.s->i 容量d[i],费用0
2.i->i+1,容量inf,费用c[i]
3.i+1->i,容量inf,费用m[i]
4.i->t 容量u[i],费用p[i]
考虑以i小->大扫描每条s->i边增广。每次流一条边i,尝试向左/右走,并取代价最小者。
实际上,这样子增广的好处在于不需要考虑向右的反向边。
在向右增广
jzoj5461 也运用到了这些性质
有一个费用流模型:
1.s->每个点i连边,费用为0,流量1
2.每个点i向t连边,费用p[i],流量1
3.每个点向新点v连边,费用q[i],流量1
4.新点v向t连边,费用0,流量k
有一个显然的做法:每次增广,然后动态维护现在的费用流图。如果费用太大则跳出循环。可以用堆来维护现在这个图。
发现增广路径只有3种:
1.从起点->i->t,前提是s->i,i->t的边未被占用。
2.从起点->i->v->j->t,前提是i->v被占用,且s->i,j->t的边未被占用,费用为p[i]-q[j]
3.从起点->i->v->t,前提是s->i,i->v的边未被占用。
其他的路径都会有环,不优秀。
发现这样子的加边不会让某条s->i的边解除占用,可以使用一个vis数组存储s->i的边的占用情况。
维护2个堆,每个堆存储优惠券的边(i->v的边),普通的边(i->t的边),
如果现在的元素被占用,则从堆中删除。
实际上,在程序中,2,3情况可以再一起考虑。
额外使用一个堆,这个堆存储如果沿着反向边走,获得的代价。
取2者较小者增广即可。
#include<bits/stdc++.h> using namespace std; priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q1,q2; priority_queue<int,vector<int>,greater<int> > q3; int n,k,vis[50010],r; long long m,p[50010],q[50010]; int main(){ freopen("shopping.in","r",stdin); freopen("shopping.out","w",stdout); scanf("%d%d%lld",&n,&k,&m); for(int i=1;i<=n;i++) scanf("%d%d",&p[i],&q[i]); for(int i=1;i<=k;i++) q3.push(0); for(int i=1;i<=n;i++){ q1.push(make_pair(p[i],i)); q2.push(make_pair(q[i],i)); } while(r<n){ while(vis[q1.top().second]) q1.pop(); while(vis[q2.top().second]) q2.pop(); if(q2.top().first+q3.top()<q1.top().first){ m-=q2.top().first+q3.top(); if(m<0)break; q3.pop(); q3.push(p[q2.top().second]-q[q2.top().second]); vis[q2.top().second]=1; q2.pop(); } else{ m-=q1.top().first; if(m<0)break; vis[q1.top().second]=1; q1.pop(); } r++; } printf("%d",r); }
NOI2019 序列
这道题的费用流模型不是很显然。且比上一道题更复杂。
对于每个点新建i',再新建a,b
1.s->每个点i连边,费用为a[i],流量1
2.每个点i'向t连边,费用b[i],流量1
3.每个i向新点a连边,费用0,流量1
4.新点a向b连边,费用0,流量l
5.b向所有点i'连边,费用0,流量1
考虑每次增加1点流量。
实际上,边只能这么走,如果走其他边会有环:
1.s->i->i'->t,表示选出一对下标相同的对,前提是i,i'都未被占用
2.s->i->a->b->j'->t 表示选出一对下标不同的对,前提是a->b的选择次数<l,i->a,b->j'都未被占用。
3.s->i->i'->b->a->j->j'->t 则把i,j都选上,并且调整一下答案让一个相同下标的边都匹配。
4.s->i->i'->b->j'->t
由费用流的重要性质1,被选出的元素不会被退。
实际上,还可能有s->i->a->b->i'->t,但是可以把它调整为s->i->i'->t,这样子也符合要求。且后面的流会更“自由”
使用一个变量cnt表示现在a->b还有多少容量。
1操作就是使用一个堆e存储a[i]+b[i]最大的,选出
2操作要使用堆a,b维护a,b最大值,并取出。当下标相同时不用减少cnt,但是当下标不同时要减。
用2个数组维护左边,右边的点被选择的情况。用另外2个堆c,d维护一个点被选后,左边/右边的点被选的情况。
对于3操作,
如果老鼠既可以向右,也可以向左走,则显然可以构建图:
1.s->每个老鼠,费用0,流量1
2.离散化,每个老鼠/洞连向它右/左边的第一个关键点,费用距离,流量无限(老鼠或洞)
3.每个洞->t,费用0,流量1
实际上,一些模拟费用流的题可以使用带权二分做。如种树。