网络流学习笔记

这篇本该放在图论里,想了想还是单独拿出来。

目录

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&&in;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&&in;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;
}

总结一下:费用流由于又加了一个费用,所以会有诸多限制要求以及选择。需要将有用信息提炼出,根据选择来建边,根据限制确定边的要素。

习题

练习可以查找这个题单里的图论部分。

原文地址:https://www.cnblogs.com/luotao0034/p/14432082.html