费用流模型
费用流板子
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define inf 0x3f3f3f3f
using namespace std;
const int N=5010,M=100100;
int n,m,S,T,cnt=1;
int dis[N],vis[N],incf[N],pre[N],head[N];
struct node{
int to,next,w,c;
}e[M];
int read(){
int x=0,f=1; char ch=getchar();
if(ch=='-') f=-1, ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
return x*f;
}
void add(int u,int v,int w,int c){
e[++cnt].to =v; e[cnt].next =head[u]; e[cnt].w=w; e[cnt].c=c; head[u]=cnt;
e[++cnt].to =u; e[cnt].next =head[v]; e[cnt].w=0; e[cnt].c=-c; head[v]=cnt;
}
bool spfa(){
queue<int>q;
memset(dis,0x3f,sizeof dis);
memset(incf,0,sizeof incf);
q.push(S); dis[S]=0; incf[S]=inf;
while(!q.empty()){
int u=q.front(); q.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].next ){
int v=e[i].to ;
if(e[i].w&&dis[v]>dis[u]+e[i].c){
dis[v]=dis[u]+e[i].c;
pre[v]=i;
incf[v]=min(e[i].w,incf[u]);
if(!vis[v]){
q.push(v);
vis[v]=1;
}
}
}
}
return incf[T]>0;
}
void EK(int& flow, int& cost){
flow=cost=0;
while(spfa()){
int t=incf[T];
flow+=t; cost+=t*dis[T];
for(int i=T;i!=S;i=e[pre[i]^1].to){
e[pre[i]].w-=t;
e[pre[i]^1].w+=t;
}
}
}
int main(){
n=read(); m=read(); S=read(); T=read();
for(int i=1;i<=m;i++){
int u=read(), v=read(), w=read(), c=read();
add(u,v,w,c);
}
int flow,cost;
EK(flow,cost);
printf("%d %d",flow,cost);
return 0;
}
费用流直接应用
运输问题
W 公司有 m 个仓库和 n 个零售商店。
第 i 个仓库有 ai 个单位的货物;第 j 个零售商店需要 bj 个单位的货物。
货物供需平衡,即 ∑ i = 1 m a i = ∑ j = 1 n b j ∑_{i=1}^{m}a_i=∑_{j=1}^{n}b_j ∑i=1mai=∑j=1nbj。
从第 i 个仓库运送每单位货物到第 j 个零售商店的费用为 cij。
试设计一个将仓库中所有货物运送到零售商店的运输方案。
对于给定的 m 个仓库和 n 个零售商店间运送货物的费用,计算最优运输方案和最差运输方案。
S连仓库(aij,0),商店连T(bij,0),每个仓库和商店之间连(inf,Cij)。
本题建图比较显然
void add(int u,int v,int w,int c){
e[++cnt].to=v; e[cnt].next=head[u]; e[cnt].w=w; e[cnt].c=c; head[u]=cnt;
e[++cnt].to=u; e[cnt].next=head[v]; e[cnt].w=0; e[cnt].c=-c; head[v]=cnt;
}
bool spfa(){}
int EK(){}
int main(){
m=read(); n=read();
S=N-1, T=N-2;
for(int i=1;i<=m;i++){
int a=read();
add(S,i,a,0);
}
for(int i=1;i<=n;i++){
int a=read();
add(m+i,T,a,0);
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
int a=read();
add(i,m+j,inf,a);
}
}
printf("%d\n",EK());
for(int i=2;i<=cnt;i+=2){
e[i].w+=e[i^1].w;
e[i^1].w=0;
e[i].c=-e[i].c;
e[i^1].c=-e[i^1].c;
}
printf("%d\n",-EK());
}
负载平衡问题
G 公司有 n 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。
如何用最少搬运量可以使 n 个仓库的库存数量相同。
搬运货物时,只能在相邻的仓库之间搬运。
要使所有仓库最终的库存数量相同,为x=tot/n,则根据贪心的思想,只需从货物多的仓库像货物少的仓库搬运。
对于所有库存数量低于最终平均数量x的仓库,从源点S向其连边(a[i]-x,0)
对于所有库存数量高于最终平均数量x的仓库,从其向汇点T连边(x-a[i],0)
每个相邻仓库之间连边(inf,1)
void add(int u,int v,int w,int c){
e[++cnt].to=v; e[cnt].next =head[u]; e[cnt].w =w ; e[cnt].c=c; head[u]=cnt;
e[++cnt].to=u; e[cnt].next =head[v]; e[cnt].w =0 ;e[cnt].c =-c;head[v]=cnt;
}
bool spfa(){}
int EK(){}
int main(){
n=read(); T=N-2;
int tot=0;
for(int i=1;i<=n;i++){
a[i]=read();
tot+=a[i];
add(i,i<n?i+1:1,inf,1);//环形连边技巧
add(i,i>1?i-1:n,inf,1);
}
tot/=n;
for(int i=1;i<=n;i++){
if(tot<a[i]) add(S,i,a[i]-tot,0); //这里tot已经变成平均值x了
if(tot>a[i]) add(i,T,tot-a[i],0);
}
printf("%d",EK());
}
二分图最大匹配
分配问题
有 n 件工作要分配给 n 个人做。
第 i 个人做第 j 件工作产生的效益为 cij。
试设计一个将 n 件工作分配给 n 个人做的分配方案。
对于给定的 n 件工作和 n 个人,计算最优分配方案和最差分配方案
跟运输问题很像 不解释
void add(int u,int v,int w,int c){
e[++cnt].to =v; e[cnt].next =head[u];e[cnt].w =w; e[cnt].c =c; head[u]=cnt;
e[++cnt].to =u; e[cnt].next =head[v];e[cnt].w =0; e[cnt].c =-c; head[v]=cnt;
}
bool spfa(){}
int EK(){}
int main(){
n=read(); T=N-1;
for(int i=1;i<=n;i++){
add(S,i,1,0);
add(i+n,T,1,0);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int c=read();
add(i,n+j,inf,c);
}
}
printf("%d\n",EK());
for(int i=2;i<=cnt;i+=2){
e[i].w+=e[i^1].w;
e[i^1].w=0;
e[i].c=-e[i].c;
e[i^1].c=-e[i^1].c;
}
printf("%d",-EK());
}
最大权不相交路径
数字梯形问题
给定一个由 n 行数字组成的数字梯形如下图所示。
梯形的第一行有 m 个数字。
从梯形的顶部的 m 个数字开始,在每个数字处可以沿左下或右下方向移动,形成一条从梯形的顶至底的路径。
规则 1:从梯形的顶至底的 m 条路径互不相交。
规则 2:从梯形的顶至底的 m 条路径仅在数字结点处相交。
规则 3:从梯形的顶至底的 m 条路径允许在数字结点相交或边相交。
对每个点设定唯一编号,并拆点。
S向第一层点连边,最后一层点向T连边
可以相互到达的点与点之间连边
点内部连边
根据三个规则对路径的限制分别修改对点的限制。
- 1:除了点内部的边(1,w) 其余所有边的权值(1,0)
- 2:点内部的边和连向T的边的容量都设为inf 因为可以重复走
- 3:继续放开对点与点之间连边的限制,除了从S出发的边之外,其余的边的容量都改为inf
void add(int u,int v,int w,int c){
e[++cnt].to =v; e[cnt].next =head[u];e[cnt].w =w; e[cnt].c =c; head[u]=cnt;
e[++cnt].to =u; e[cnt].next =head[v];e[cnt].w =0; e[cnt].c =-c; head[v]=cnt;
}
void cl(){
cnt=1;
memset(e,0,sizeof e);
memset(head,0,sizeof head);
}
bool spfa(){}
int EK(){}
int main(){
m=read(); n=read();
int ct=0;
S=++ct; T=++ct;
for(int i=1;i<=n;i++){
for(int j=1;j<=m+i-1;j++){
id[i][j]=++ct;
w[i][j]=read();
}
}
cl();
for(int i=1;i<=n;i++){
for(int j=1;j<=m+i-1;j++){
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,1,0);
add(id[i][j]*2,id[i][j]*2+1,1,w[i][j]);
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
printf("%d\n",EK());
cl();
for(int i=1;i<=n;i++){
for(int j=1;j<=m+i-1;j++){
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,inf,0);
add(id[i][j]*2,id[i][j]*2+1,inf,w[i][j]);
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
printf("%d\n",EK());
cl();
for(int i=1;i<=n;i++){
for(int j=1;j<=m+i-1;j++){
if(i==1) add(S,id[i][j]*2,1,0);
if(i==n) add(id[i][j]*2+1,T,inf,0);
add(id[i][j]*2,id[i][j]*2+1,inf,w[i][j]);
add(id[i][j]*2+1,id[i+1][j]*2,inf,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,inf,0);
}
}
printf("%d\n",EK());
}
费用流网格图模型
K取方格数
在一个 N×N 的矩形网格中,每个格子里都写着一个非负整数。
可以从左上角到右下角安排 K 条路线,每一步只能往下或往右,沿途经过的格子中的整数会被取走。
若多条路线重复经过一个格子,只取一次。
求能取得的整数的和最大是多少。
最大流限制为K,求最大费用
在网格图中我们常用Get()返回的hash值来获取点的唯一编号,上一道题中采用的矩阵存储也是一种方式,但hash较为节省空间。
S向(1,1)点连边(k,0); (n,n)点向T连边(k,0)
由于方格中的每个数只能取一次,但可以重复走,所以可以在拆点,在点内部连两种边,一种是(1,c)另一种(inf,0),以此满足题意
点与点之间之间没有限制,即可直接在互相可以到达的点之间连(inf,0)
void add(int u,int v,int w,int c){
e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt; e[cnt].w=w; e[cnt].c=c;
e[++cnt].to=u; e[cnt].next=head[v]; head[v]=cnt; e[cnt].w=0; e[cnt].c=-c;
}
int get(int x,int y,int t){
return ((x-1)*n+y)*2+t;
}
bool spfa(){}
int EK(){}
int main(){
n=read(); k=read();
S=N-1, T=N-2;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int c=read();
add(get(i,j,0),get(i,j,1),1,c);
add(get(i,j,0),get(i,j,1),inf,0);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i+1<=n) add(get(i,j,1),get(i+1,j,0),inf,0);
if(j+1<=n) add(get(i,j,1),get(i,j+1,0),inf,0);
}
}
add(S,get(1,1,0),k,0);
add(get(n,n,1),T,k,0);
printf("%d",EK());
}
深海机器人问题
深海资源考察探险队的潜艇将到达深海的海底进行科学考察。
潜艇内有多个深海机器人。
潜艇到达深海海底后,深海机器人将离开潜艇向预定目标移动。
深海机器人在移动中还必须沿途采集海底生物标本。
沿途生物标本由最先遇到它的深海机器人完成采集。
每条预定路径上的生物标本的价值是已知的,而且生物标本只能被采集一次。
本题限定深海机器人只能从其出发位置沿着向北或向东的方向移动,而且多个深海机器人可以在同一时间占据同一位置。
给定每个深海机器人的出发位置和目标位置,以及每条网格边上生物标本的价值。
计算深海机器人的最优移动方案,使尽可能多的深海机器人到达目的地的前提下,采集到的生物标本的总价值最高。
网格图 get()套路
由于每个边的权值只能取一次,同上一题一样,建两种边,不过不同的是,本题的权值在边上而上一题的在点上。所以本题免去了拆点的麻烦
向每个相互可到达的点之间建好边之后,其余的就是一个多源汇的最大费用最大流问题了
void add(int u,int v,int w,int c){
e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt; e[cnt].w=w; e[cnt].c=c;
e[++cnt].to=u; e[cnt].next=head[v]; head[v]=cnt; e[cnt].w=0; e[cnt].c=-c;
}
int get(int x,int y){
return x*(m+1)+y;
}
bool spfa(){}
int EK(){}
int main(){
int a,b;
a=read(); b=read();
n=read(); m=read();
T=N-1; S=N-2;
for(int i=0;i<=n;i++){
for(int j=0;j<m;j++){
int c=read();
add(get(i,j),get(i,j+1),1,c);
add(get(i,j),get(i,j+1),inf,0);
}
}
for(int i=0;i<=m;i++){
for(int j=0;j<n;j++){
int c=read();
add(get(j,i),get(j+1,i),1,c);
add(get(j,i),get(j+1,i),inf,0);
}
}
while(a--){
int k=read(),x=read(),y=read();
add(S,get(x,y),k,0);
}
while(b--){
int k=read(),x=read(),y=read();
add(get(x,y),T,k,0);
}
printf("%d",EK());
}
费用流拆点
餐巾计划问题
本题的建图方式比较抽象。
一个餐厅在相继的 N 天里,每天需用的餐巾数不尽相同。
假设第 i 天需要 ri 块餐巾 (i=1,2,…,N)。
餐厅可以购买新的餐巾,每块餐巾的费用为 p 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n 天,其费用为 s 分。
餐厅每天使用的餐巾必须是今天刚购买的,或者是今天刚洗好的,且必须恰好提供 ri 块毛巾,不能多也不能少。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。
但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 N 天中餐巾使用计划,使总的花费最小。
拆点,把每个点拆成出和入,也就是将每天的需求和“产出”分开。
S向每天的入点连边(inf,p) ,相当于直接购买
i-1天的出边向第i天的入边连边(inf,f)(inf,s) 相当于是送到快洗部和慢洗部
每天的脏餐巾可以留到下一天,从i-1到第i天的出点连边(inf,0)
从源点S向每天的出点连边(ri,0),对应每天用剩的餐巾是ri个
每天的出点向汇点T连边(ri,0) ,对应每天的需求是ri
void add(int u,int v,int w,int c){
e[++cnt].to =v; e[cnt].next =head[u]; head[u]=cnt; e[cnt].c=c; e[cnt].w=w;
e[++cnt].to =u; e[cnt].next =head[v]; head[v]=cnt; e[cnt].c=-c; e[cnt].w=0;
}
bool spfa(){}
int EK(){}
int main(){
int p,fp,f,s,sp;
n=read(); p=read(); f=read(); fp=read(); s=read(); sp=read();
S=0,T=N-1;
for(int i=1;i<=n;i++){
int r=read();
add(S,i,r,0);
add(S,i+n,inf,p);
if(i+f<=n) add(i,n+i+f,inf,fp);
if(i+s<=n) add(i,n+i+s,inf,sp);
if(i+1<=n) add(i,i+1,inf,0);
add(n+i,T,r,0);
}
printf("%d",EK());
}
费用流上下界可行流
志愿者招募
AcWing 969. 志愿者招募
本题的建图依旧是非常抽象
申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。
布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。
经过估算,这个项目需要 N 天才能完成,其中第 i 天至少需要 Ai 个人。
布布通过了解得知,一共有 M 类志愿者可以招募。
其中第 i 类可以从第 Si 天工作到第 Ti 天,招募费用是每人 Ci 元。
新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长!
于是布布找到了你,希望你帮他设计一种最优的招募方案。
将每天的志愿者需求人数下限看做流量下界,将可以从第 Si 天工作到第 Ti 天的志愿者看做是网络中的一条回流(保证流量守恒的同时可以方便地得出流量和费用)。
注意这里每一天对应的是一条从第i到第i+1个点的边,因此有1~n+1个点
那么套用上下界可行流的模板。注意上界是inf
对于新图的每个边 f ′ ( u , v ) = c u ( u , v ) − c l ( u , v ) f'(u,v)=c_u(u,v)-c_l(u,v) f′(u,v)=cu(u,v)−cl(u,v) 也就是上界-下界
每个点 A [ i ] = C i n − C o u t A[\ i\ ]= C_{in} -C_{out} A[ i ]=Cin−Cout
对于每个 A [ i ] < 0 A[\ i\ ]<0 A[ i ]<0 的点u 建一条v–>T
对于每个 A [ i ] > 0 A[\ i\ ]>0 A[ i ]>0 的点v 建一条S–>u
再加志愿者的回流边,即从第Ti+1个点向第Si个点连边(inf,ci)
void add(int u,int v,int w,int c){
e[++cnt].to =v; e[cnt].next =head[u]; head[u]=cnt; e[cnt].w=w; e[cnt].c=c;
e[++cnt].to =u; e[cnt].next =head[v]; head[v]=cnt; e[cnt].w=0; e[cnt].c=-c;
}
bool spfa(){}
int EK(){}
int main(){
n=read(), m=read();
T=N-1;
int last=0,cur;
for(int i=1;i<=n;i++){
cur=read();
if(cur-last<0) add(S,i,last-cur,0);
if(cur-last>0) add(i,T,cur-last,0);
add(i,i+1,inf-cur,0);//上界-下界
last=cur;
}
add(n,n+1,inf-cur,0);//上界-下界
add(S,n+1,cur,0); //第n+1个点只有C_in=cur 没有C_out 因此从S连
//上面两个加边与前面的同理 注意不是特判
for(int i=1;i<=m;i++){
int a=read(),b=read(),s=read();
add(b+1,a,inf,s); //志愿者的回流边
}
printf("%d",EK());
}