zoukankan      html  css  js  c++  java
  • 网络流学习笔记

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

    目录

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

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

    习题

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

  • 相关阅读:
    oracle登录错误(ORA-01033:ORACLE initialization or shutdown in progress
    mssql 判断sql语句的执行效率语句
    关于 knockout js 学习中的疑问 (1)
    JS 根据Url参数名称来获取对应的值 方法封装
    账户
    windows库
    CentOS的Qt3和Qt4问题
    c/c++调用dll
    CentOS 安装g++
    输入法不见了
  • 原文地址:https://www.cnblogs.com/luotao0034/p/14432082.html
Copyright © 2011-2022 走看看