2019.8.1金华暑假集训Day5讲课总结

吐槽

我终于感觉不像前几天那么的难了,或许是换成了个女老师,或许是因为在衢州集训的时候是她再讲,所以风格还是比较清楚,或许是太水了

网络流

背景(纯属复制)

图论中的一种理论与方法,研究网络上的一类最优化问题 。1955年 ,T.E.哈里斯在研究铁路最大通量时首先提出在一个给定的网络上寻求两点间最大运输量的问题。1956年,L.R. 福特和 D.R. 富尔克森等人给出了解决这类问题的算法,从而建立了网络流理论。所谓网络或容量网络指的是一个连通的赋权有向图 D= (V、E、C) , 其中V 是该图的顶点集,E是有向边(即弧)集,C是弧上的容量。此外顶点集中包括一个起点和一个终点。网络上的流就是由起点流向终点的可行流,这是定义在网络上的非负函数,它一方面受到容量的限制,另一方面除去起点和终点以外,在所有中途点要求保持流入量和流出量是平衡的。如果把下图看作一个公路网,顶点v1…v6表示6座城镇,每条边上的权数表示两城镇间的公路长度。现在要问 :若从起点v1将物资运送到终点v6去 ,应选择那条路线才能使总运输距离最短?这样一类问题称为最短路问题 。 如果把上图看作一个输油管道网 , v1 表示发送点,v6表示接收点,其他点表示中转站 ,各边的权数表示该段管道的最大输送量。现在要问怎样安排输油线路才能使从v1到v6的总运输量为最大。这样的问题称为最大流问题。
最大流理论是由福特和富尔克森于 1956 年创立的 ,他们指出最大流的流值等于最小割(截集)的容量这个重要的事实,并根据这一原理设计了用标号法求最大流的方法,后来又有人加以改进,使得求解最大流的方法更加丰富和完善 。最大流问题的研究密切了图论和运筹学,特别是与线性规划的联系,开辟了图论应用的新途径。
假设G(V,E) 是一个有限的有向图,它的每条边(u,v)∈E都有一个非负值实数的容量c(u,v) 。如果(u,v)不属于E,我们假设c(u,v) = 0 。我们区别两个顶点:一个源点s和一个汇点t。一道网络流是一个对于所有结点u和v都有以下特性的实数函数f:V×V→R 容量限制 (Capacity Constraints): f(u,v)≤c(u,v)一条边的流不能超过它的容量。 斜对称 (Skew Symmetry): f(u,v)=-f(v,u)由u到v的净流必须是由v到u的净流的相反(参考例子)。(既然要看网络流,这是一定要知道的) 流守恒 (Flow Conservation): 除非u=s或u=t,否则Σ(w∈V)f(u,w)=0 一结点的净流是零,除了“制造”流的源点和“消耗”流的汇点。 注意f(u,v) 是由u到v的净流。如果该图代表一个实质的网络,由u到v有 4 单位的实际流及由v到u有 3 单位的实际流,那么f(u,v) = 1 及f(v,u) = − 1。
边的剩余容量 (Residual Capacity)是cf(u,v) =c(u,v)−f(u,v)。这定义了以Gf(V,Ef)表示的剩余网络 (Residual Network),它显示可用的容量的多少。留意就算在原网络中由u到v没有边,在剩余网络乃可能有由u到v的边。因为相反方向的流抵消,减少由v到u的流等于增加由u到v的流。扩张路径 (Augmenting Path)是一条路径 (u1,u2...uk),而u1 =s、uk=t及cf(ui,ui+ 1) > 0,这表示沿这条路径传送更多流是可能的。

最大流

最大流问题,即是找出一个满足上述条件的f,使得∑(V∈V)f(s,v)最大化

Edmonds-Karp算法

这个算法的步骤流程如下:

先做一遍BFS,查找一个最大流,如果没有,就跳出,如果有就把他的路程记录下来,最后进行修改操作,把反向边加让流量,让正向边减去即可。具体细节看如下代码:

#include<bits/stdc++.h>
using namespace std;
const int N=10010;
struct cow{int x,y,z;}e[N*10<<1];
int n,m,head[N],tot=1,s,t;
void inse(int xxxx,int yyyy,int zzzz)
{
	e[++tot].x=head[xxxx];
	head[xxxx]=tot;
	e[tot].y=yyyy;
	e[tot].z=zzzz;
}
queue<int>q;int d[N],pre[N];
bool v[N]; 
bool bfs()//求解是否有增广路 
{
	memset(v,0,sizeof(v));
	while(!q.empty())q.pop(); 
	d[s]=INT_MAX;//d[x]表示x号点的目前的容量,原点为正无穷 
	q.push(s);v[s]=1;
	while(!q.empty())
	{
		int x=q.front();q.pop();
		for(int i=head[x];i;i=e[i].x)
		{
			int y=e[i].y,z=e[i].z;
			if(!z||v[y])continue;//如果这个点被访问过或者权值为0(没有容量),那么就跳出 
			d[y]=min(z,d[x]);//这一个点的容量就是到前一个点的容量1,和自己本身的容量取较小值
			pre[y]=i;//记录前驱,注意不是记录前驱的点,是记录边,更加有利于找到最长路的实际方案 
			q.push(y);v[y]=1;//加入队列,标记 
			if(y==t)return 1;//如果走到终点,就说明有增广路  
		}
	}
	return 0;//没有找到增广路 
}
int maxflow;
void update()
{
	int x=t;
	while(x!=s)
	{
		int i=pre[x];//每次往回回溯,修改
		e[i].z-=d[t];//正向边-最大值,反向边+最大值 
		e[i^1].z+=d[t];//利用xor的技巧 
		x=e[i^1].y;//将x赋值成他的上一个点 
	}
	maxflow+=d[t];//统计答案 
}
int main()
{
	//算法时间复杂度O(NM^2)但是远远不会到达这个上界,但是不常用,一般处理10^3~10^4的问题 
	//这个算法由于每一次只处理一条边所以复杂度较高 
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		inse(x,y,z);inse(y,x,0);
	}
	while(bfs())update();
	cout<<maxflow;
	return 0;
}

Dinic算法

算法步骤如下:

先做一遍BFS,求解出一个分层图,顺便判断是否会有新的流量出现,再在dfs中处理一大块的流量从而达到目的,在DFS中,还可以加上大量的剪枝,具体的看代码:

#include<bits/stdc++.h>
using namespace std;
const int N=10010;
struct cow{int x,y,z;}e[N*10<<1];
int n,m,head[N],tot=1,s,t;
void inse(int xxxx,int yyyy,int zzzz)
{
	e[++tot].x=head[xxxx];
	head[xxxx]=tot;
	e[tot].y=yyyy;
	e[tot].z=zzzz;
}
queue<int>q;int d[N];
bool bfs()//求解是否有增广路,顺便处理一下分层图 
{
	memset(d,0,sizeof(d));
	while(!q.empty())q.pop();//要清空!!!!(下面有直接return) 
	d[s]=1;//d[x]表示x号点的深度,原点为1 
	q.push(s);
	while(!q.empty())
	{
		int x=q.front();q.pop();
		for(int i=head[x];i;i=e[i].x)
		{
			int y=e[i].y,z=e[i].z;
			if(!z||d[y])continue;//如果这个点被访问过或者权值为0(没有容量),那么就跳出 
			d[y]=d[x]+1;//计算深度 
			q.push(y);//加入队列,标记 
			if(y==t)return 1;//如果走到终点,就说明有增广路  
		}
	}
	return 0;//没有找到增广路 
}
int maxflow;
int dinic(int x,int flow)//当前图上增广,x表示当前点,flow表示从s做到现在的x的容量 
{
	if(x==t)return flow;//到达终点
	int rest=flow,k;
	for(int i=head[x];i&&rest;i=e[i].x)
	{//当rest为0时,只能跳出循环了(没有剩下的了) 
		int y=e[i].y,z=e[i].z;
		if(!z||d[y]!=d[x]+1)continue;
		k=dinic(y,min(rest,z));//下一个的容量为x点的容量和这个点的容量的较小值
		if(!k)d[y]=0;//剪枝,去掉已经增广的点
		e[i].z-=k;e[i^1].z+=k;//当前点-,反向边+
		rest-=k;//计算还剩下的容量 
	} 
	return flow-rest;//当前的容量-还剩的容量就是x~t的最大流量 
} 
int main()
{
	//算法时间复杂度O(N^2M)但是远远不会到达这个上界,一般处理10^4~10^5的问题,由于边数基本都大于点数,所以这个更加常用  
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		inse(x,y,z);inse(y,x,0);
	}
	int flow=0;
	while(bfs())while(flow=dinic(s,INT_MAX))maxflow+=flow; 
	//第二个while的跳出只有dinic函数里的rest=flow=INT_MAX时,也就是没有增广路 
	cout<<maxflow;
	return 0;
}

最大流的建模

最小割

定义

对于一个图G=(V,E),源点为S,汇点为T,若编集E‘∈E被删掉后,使得原图不连通,那么称为割,边的容量之和最小的称为最小割

最大流最小割定理

这个就是指最大流=最小割

证明:

假设最小割>最大流,那么去掉最大流的那些边后就无法组成了,比假设的最小割小,所以不成立

假设最小割<最大流,那么在去掉所有最小割的边后,就一定还有一条增广路使得这个图联通,不成立

证毕

费用流

定义

也叫作最小费用最大流,是指在普通的网络流图中,每条边的流量都有一个单价,求出一组可行解,使得在满足它是最大流的情况下,总的费用最小。 

Edmonds-Karp增广路算法

这种算法和原来的最大流算法一样,就是把BFS求解增广路变成用spfa求解一条费用最小的增广路(为什么不能用DJ,因为反向建边的时候这个价值是负数,不能做)代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=5010;
struct cow{int x,y,z,w;}e[N*10<<1];
int n,m,head[N],tot=1,s,t;
void inse(int xxxx,int yyyy,int zzzz,int wwww)
{
	e[++tot].x=head[xxxx];
	head[xxxx]=tot;
	e[tot].y=yyyy;
	e[tot].z=zzzz;
	e[tot].w=wwww;
}
queue<int>q;int d[N],pre[N],sum[N];bool v[N]; 
bool spfa()//spfa求解是否有增广路和费用最小的增广路 ,就是将费用作为价值跑最短路 
{
	memset(v,0,sizeof(v));
	while(!q.empty())q.pop(); 
	memset(sum,10,sizeof(sum));
	//这个要清零因为后面的return要判断 
	d[s]=INT_MAX;sum[s]=0;
	q.push(s);v[s]=1;
	while(!q.empty())
	{
		int x=q.front();q.pop();v[x]=0;
		for(int i=head[x];i;i=e[i].x)
		{
			int y=e[i].y,z=e[i].z,w=e[i].w;
			if(!z||sum[y]<=sum[x]+w)continue;//容量为0
			sum[y]=sum[x]+w;
			d[y]=min(z,d[x]);pre[y]=i;
			if(!v[y])v[y]=1,q.push(y);
		}
	}
	return sum[t]!=168430090;
}
int maxflow,ans;
void update()
{
	int x=t;
	while(x!=s)
	{
		int i=pre[x];
		e[i].z-=d[t];
		e[i^1].z+=d[t];
		x=e[i^1].y;
	}
	ans+=d[t]*sum[t]; 
	maxflow+=d[t];//统计答案
}
int main()
{
	//这个算法由于每一次只处理一条边所以复杂度较高,不怎么实用 
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++)
	{
		int x,y,z,w;
		scanf("%d%d%d%d",&x,&y,&z,&w);
		inse(x,y,z,w);inse(y,x,0,-w);
	}
	while(spfa())update();
	cout<<maxflow<<' '<<ans;
	return 0;
}

 

 

总结

网络流的难点不在于他的模板,是如何套用这个模板,合理地建模是网络流快慢的重要因素

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值