这篇本该放在图论里,想了想还是单独拿出来。
目录
1.基本概念
2.最大流
3.最小割
4.费用流
5.建模
- 最大流的匹配问题
- 最小割的二选一问题
- 费用流
6.习题
由于会的东西不多,暂时只讲最简单的。本人理解粗糙,如有误,还望指出。
基本概念
在讲网络流的经典问题之前,先了解一下基本概念。
网络流的图尽管长得和普通的图一样,但是数字代表的意义不一样。每张图的起点称为源点,终点称为汇点,边上的数字称为这条边的容量。这是基本概念。在这条边实际流过的量称为这条边的流量,显然流量(le)容量。
对于每一条可以从源点抵达汇点的道路,我们称其为增广路。显然,一条增广路的流量受它经过的所有边中容量最小的那条边限制。就如同一根粗细不一的水管,每次能流出的水总是被最细的地方限制。
另外需要知道的一点是,源点只会出水,不会入水,汇点只入水不出水,中间点出入的流量相同。
最大流
最大流问题,就是问在源点的水无限的情况下,汇点最多可以接受到的流量为多少。
常用的有两种算法,分别是EK与dinic。
EK算法
EK算法的核心思想就是不停寻找增广路,直到找不到为止。
每次找到一条增广路之后,对这条路上所有的边的容量进行更新,正向边容量减去流量,反向边加上流量。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=200+5,M=1e5+10;
int n,m,s,t;
int tot;
int vis[M];
int head[M],ver[M],Next[M];
long long c[M];
void add(int x,int y,int z)
{
ver[++tot]=y;c[tot]=z;Next[tot]=head[x];head[x]=tot;
ver[++tot]=x;c[tot]=0;Next[tot]=head[y];head[y]=tot;
}
const int inf=1<<29;
int pre[N];
long long incf[N];
bool bfs()
{
memset(vis,0,sizeof(vis));
queue<int>q;
q.push(s);vis[s]=1;
incf[s]=inf;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=Next[i])
{
if(c[i]){
int v=ver[i];
if(vis[v])continue;
incf[v]=min(incf[u],c[i]);
q.push(v);vis[v]=1;
pre[v]=i;
if(v==t)return 1;
}
}
}
return 0;
}
long long maxflow;
void update()
{
int u=t;
while(u!=s)
{
int i=pre[u];
c[i]-=incf[t];
c[i^1]+=incf[t];
u=ver[i^1];
}
maxflow+=incf[t];
}
int main()
{
tot=1;
scanf("%d%d%d%d",&n,&m,&s,&t);
while(m--)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
while(bfs())update();
cout<<maxflow;
return 0;
}
dinic算法
核心思想同样是找增广路。但是与EK不同在于是用dfs找增广路,可以同时进行多路增广,并且边更新。
但是因为dfs限制少,所以每次进行dfs前需要用bfs对图进行分层,dfs时限制当前点只能往下一层的点进行dfs。
代码(加当前弧优化)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define ll long long
using namespace std;
const int N=2e5+5,M=2e5+5;
int n,m,s,t;
int head[N],ver[M],Next[N],tot;
ll val[M];
void add(int x,int y,ll z)
{
ver[++tot]=y;val[tot]=z;
Next[tot]=head[x];head[x]=tot;
}
int dep[N],now[N];
bool bfs()
{
memset(dep,0,sizeof(dep));
queue<int>q;
q.push(s);
dep[s]=1;
now[s]=head[s];
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=Next[i])
{
int v=ver[i];
if(!val[i]||dep[v])continue;
now[v]=head[v];
dep[v]=dep[u]+1;
q.push(v);
}
}
return dep[t];
}
ll dfs(int u,ll in)
{
if(u==t)return in;
ll out=0;
for(int i=now[u];i&∈i=Next[i])
{
now[u]=i;
int v=ver[i];
if(val[i]&&dep[v]==dep[u]+1)
{
ll res=dfs(v,min(in,val[i]));
val[i]-=res;
val[i^1]+=res;
in-=res;
out+=res;
}
}
if(out==0)dep[u]=0;
return out;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
tot=1;
while(m--)
{
int u,v;ll w;
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
add(v,u,0);
}
ll ans=0;
while(bfs())
{
ans+=dfs(s,1e18);
}
printf("%lld",ans);
return 0;
}
最小割
做此类题需要知道一个定理:最小割=最大流。
证明不会,但是可以意会,割就是可以使源点与汇点不连通的边的集合。最小割就是权值和最小的割。最小割里的边都是增广路中容量最小的边,因为割掉这些边就会使图不连通,所以最大流=这些边的容量之和。
然后最小割问题转化为最大流即可。
费用流
费用流一般有两种:EK/dinic+spfa或是zkw费用流。
一般适用性较广的是EK+spfa与zkw费用流。
zkw费用流还没学,所以这里只讲EK+spfa。
此算法与原本EK的不同之处就在于bfs函数,由普通的bfs变成了能求最短路的spfa,此时就是把这条道路的费用当成路的长度。求最短路得到的就是最小费用,最长路就是最大费用。
费用流其实是最大费用最大流/最小费用最大流的简称,因此这里的最大最小费用都建立在最大流的基础上。一条路的费用指的是流过一流量要花的费用,因此我们找到一条增广路后,用它的长度乘以它的流量即可。
具体看代码。
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=2e5+10;
int n,m,s,t;
int ver[N],Next[N],head[N],tot=1,dis[N],val[N];
void add(int x,int y,int z,int w)
{
ver[++tot]=y;val[tot]=z;dis[tot]=w;
Next[tot]=head[x];head[x]=tot;
}
int pre[N],vis[N],incf[N],d[N];
bool bfs()
{
memset(vis,0,sizeof(vis));
memset(d,0x3f,sizeof(d));
queue<int>q;
d[s]=0;vis[s]=1;
q.push(s);incf[s]=1e9;
while(q.size())
{
int u=q.front();q.pop();
vis[u]=0;
for(int i=head[u];i;i=Next[i])
{
int v=ver[i];
if(val[i]&&d[v]>d[u]+dis[i])
{
d[v]=d[u]+dis[i];
incf[v]=min(incf[u],val[i]);
pre[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
if(d[t]>1e9)return 0;
return 1;
}
int maxflow,mincost;
void update()
{
int u=t;
while(u!=s)
{
int i=pre[u];
val[i]-=incf[t];
val[i^1]+=incf[t];
u=ver[i^1];
}
maxflow+=incf[t];
mincost+=d[t]*incf[t];
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
while(m--)
{
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,c,d);
add(b,a,0,-d);
}
while(bfs())update();
printf("%d %d",maxflow,mincost);
return 0;
}
建模
网络流的建模才是它最难的地方。以上内容只讲了板子,实际上你也只需要会那些就行了。因为网络流更多的是在建图上做文章,最难的就是如何利用网络流的特性建模去解决千奇百怪的实际问题。
最大流的匹配问题
简单的最大流题目通常有两类东西用以匹配,现以试题库问题为例。
这道题中有两个要素:题目和类别。我们需要将题目和类别进行匹配,来满足题目的要求。
通用的解决方法是建立超级源点与超级汇点,源点向每道题连一条容量为一的边,每道题向可以对应的类型连一条容量为一的边,为一是因为每道题只能用一次。
然后每个类型向汇点连容量为要求数量的边。如果这些边都满流了,说明有解决方案。
再找哪些题的边容量变0即可知道用了哪些题,同理可以通过题和类型间的边知道这道题充当了哪个类别。
具体可见这个博客。
类似的匹配题目还有很多,如圆桌问题,飞行员配对方案问题等,二分图最大匹配的题也可套用此模型通过。
最小割的二选一问题
典型例题如文理分科。
问你能够获得的最大值。利用最小割则是正难则反的思路,通过找出最少失去的满意值并与所有的满意值相减得到最大值。
处理选文理科很简单,只要让源点连向每个学生,容量是文科的满意值(或理科),汇点连学生的则是理科的。
这时想要使图不连通,就必须在每个学生的两条边中选一个容量小的割掉,留下来的边容量加起来即为答案。
但是还有一种情况,一个学生与相邻的所有学生选同一学科时有额外值。要得到这个额外值,就必须保证这五个学生同一科。
(假设是都选文科的额外值)
有一种方法,就是新建一个点,这个点连向那五个学生,且这五条边容量为inf,可保证不被割掉。然后再由这个点向文科对应的源点连容量为额外值的边。
为什么这样对呢?
可以想象一下,inf的边是割不断的,只要有一个学生选了理科,那它和汇点连的边就没断,如果不断源点与新建点的边,源点和汇点就仍然联通。若要得到额外值,即留下新建点与源点的边,那么五个学生与汇点连的边必须全断,也就保证全选文科了。
代码实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=110,M=2e6+10,inf=1<<31-1;
int n,m,s,t;
int dx[5]={0,1,0,-1,0};
int dy[5]={0,0,1,0,-1};
int ver[M],head[M],Next[M],val[M],tot=1;
void add(int x,int y,int zo)
{
ver[++tot]=y;val[tot]=zo;
Next[tot]=head[x];head[x]=tot;
}
int getid(int x,int y)
{
return m*(x-1)+y;
}
int dep[M],now[M];
bool bfs()
{
memset(dep,0,sizeof(dep));
queue<int>q;
q.push(s);dep[s]=1;now[s]=head[s];
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=Next[i])
{
int v=ver[i];
if(dep[v]||!val[i])continue;
dep[v]=dep[u]+1;
now[v]=head[v];
q.push(v);
}
}
return dep[t];
}
int dfs(int u,int in)
{
if(u==t)return in;
int out=0;
for(int i=head[u];i&∈i=Next[i])
{
now[u]=i;
int v=ver[i];
if(dep[v]==dep[u]+1&&val[i])
{
int res=dfs(v,min(in,val[i]));
val[i]-=res;
val[i^1]+=res;
in-=res;
out+=res;
}
}
if(out==0)dep[u]=0;
return out;
}
int main()
{
scanf("%d%d",&n,&m);
s=0;t=n*m+1;
int sum=0,z=n*m+1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
int art,id;
scanf("%d",&art);
sum+=art;
id=getid(i,j);
add(s,id,art);
add(id,s,0);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
int sci,id;
scanf("%d",&sci);
sum+=sci;
id=getid(i,j);
add(id,t,sci);
add(t,id,0);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
int sart,id;
scanf("%d",&sart);
sum+=sart;z++;
add(s,z,sart);
add(z,s,0);
for(int k=0;k<5;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x<1||y<1||x>n||y>m)continue;
id=getid(x,y);
add(z,id,inf);
add(id,z,0);
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
int ssci,id;
scanf("%d",&ssci);
sum+=ssci;z++;
add(z,t,ssci);
add(t,z,0);
for(int k=0;k<5;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x<1||y<1||x>n||y>m)continue;
id=getid(x,y);
add(id,z,inf);
add(z,id,0);
}
}
int ans=0;
while(bfs())ans+=dfs(s,inf);
printf("%d
",sum-ans);
return 0;
}
做最小割的题主要思路就是如此。
还有一个实用的小技巧,拆点。最常见的拆点就是将一个点拆成两个点,一个专门接受指向它的边,一个专门发出指向其它点的边,两点之间再连一条边,如此可以将割点转换成割边。
拆点的泛用性很广,不止在最小割,在很多网络流的题中都能发挥奇效。个别题中,可以根据题目中的状态将一个点拆成n个点。
费用流
不知道该怎么起名了,费用流做的少,似乎没有什么经典模型。这里以[国家集训队]航班安排为例。
首先将每个请求看作一个点,然后对请求进行拆点。拆出的两点间连容量为1,费用为价值的边。
其次请求连边。飞机要满足能在时间限制T内飞回0机场,即请求结束时刻+t[请求终点机场][0](le)T。若能满足就该请求向汇点连容量为inf,费用为-f[终点][0]的边。
同理如果能在开始时刻前从0机场抵达起点,就由源点向其连边。
最后是请求之间的连边,如果这个请求结束后能在时间内到达另一个请求的起点,就连边。
再建一个新源点,向原来的源点连一条容量k,费用0的边,表明只有k架飞机。
建好图后跑一遍最大费用最大流即可。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=2e5+10,inf=1<<29;
int n,m,s,ed,k,st,T,t[201][201],f[201][201];
struct node{
int a,b,s,t,c;
}q[N];
int ver[N],Next[N],val[N],head[N],c[N],tot=1;
void add(int x,int y,int z,int w)
{
ver[++tot]=y;val[tot]=z;c[tot]=w;Next[tot]=head[x];head[x]=tot;
ver[++tot]=x;val[tot]=0;c[tot]=-w;Next[tot]=head[y];head[y]=tot;
}
int dis[N],vis[N],pre[N],incf[N];
bool bfs()
{
memset(vis,0,sizeof(vis));
memset(dis,-0x3f,sizeof(dis));
queue<int>q;
q.push(s);dis[s]=0;vis[s]=1;incf[s]=1e9;
while(!q.empty())
{
int u=q.front();q.pop();
vis[u]=0;
for(int i=head[u];i;i=Next[i])
{
int v=ver[i];
if(dis[v]<dis[u]+c[i]&&val[i])
{
dis[v]=dis[u]+c[i];
incf[v]=min(incf[u],val[i]);
pre[v]=i;
if(!vis[v]){
vis[v]=1;q.push(v);
}
}
}
}
if(dis[ed]<-1e9)return 0;
return 1;
}
int update()
{
int u=ed;
while(u!=s)
{
int i=pre[u];
val[i]-=incf[ed];
val[i^1]+=incf[ed];
u=ver[i^1];
}
return dis[ed]*incf[ed];
}
int main()
{
scanf("%d%d%d%d",&n,&m,&k,&T);
s=2*m+1;st=2*m+2;ed=2*m+3;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&t[i][j]);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&f[i][j]);
for(int i=1;i<=m;i++)
scanf("%d%d%d%d%d",&q[i].a,&q[i].b,&q[i].s,&q[i].t,&q[i].c);
for(int i=1;i<=m;i++)
{
add(i*2-1,i*2,1,q[i].c);
if(q[i].t+t[q[i].b][0]<=T)add(i*2,ed,inf,-f[q[i].b][0]);
else continue;
if(t[0][q[i].a]<=q[i].s)add(st,2*i-1,inf,-f[0][q[i].a]);
for(int j=1;j<=m;j++)
{
if(q[i].t+t[q[i].b][q[j].a]<=q[j].s)
add(2*i,2*j-1,inf,-f[q[i].b][q[j].a]);
}
}
add(s,st,k,0);
int ans=0;
while(bfs())ans+=update();
printf("%d",ans);
return 0;
}
总结一下:费用流由于又加了一个费用,所以会有诸多限制要求以及选择。需要将有用信息提炼出,根据选择来建边,根据限制确定边的要素。
习题
练习可以查找这个题单里的图论部分。