ZROI-2019.8.1

网络流算法。
本文部分参考这篇博客
首先便是定义。
两个特殊节点源(s)与汇(t),两者都是有且仅有一个;弧;弧的容量c(u,v);弧上流量f(u,v);链;割。

弧分为很多类型。

  • 饱和弧: f ( u , v ) = c ( u , v ) f(u,v)=c(u,v) f(u,v)=c(u,v)
  • 非饱和弧: f ( u , v ) < c ( u , v ) f(u,v) \lt c(u,v) f(u,v)<c(u,v)
  • 零流弧: f ( u , v ) = 0 f(u,v)=0 f(u,v)=0
  • 非零流弧: f ( u , v ) > 0 f(u,v) \gt 0 f(u,v)>0

链的定义:在容量网络中,称一个顶点序列为链应该满足任意相邻两个点之间都有弧相连,但是弧的方向不一定都要和链的方向相同。

割的定义:对于网络流图的一个子集,如果它把点分为了两个部分,那么那些从一部分到另一部分的边所组成的集合,就是割。

再是性质。

  • 容量限制:对于任意 u , v ∈ V u,v \in V u,vV 0 ≤ f ( u , v ) ≤ c ( u , v ) 0 \le f(u,v) \le c(u,v) 0f(u,v)c(u,v)
  • 流量守恒:对任意非S和T节点 u u u,有 ∑ f ( u , v ) = 0 \sum f(u,v) =0 f(u,v)=0

关于流的定义。

  • 可行流:满足流量限制和平衡条件的流是可行流。
  • 零流:如果网络流上每条弧的流量都为0,就被成为零流。
  • 伪流:即只满足流量限制的流。该流对预流推进算法有作用。
  • 最大流:流量最大的可行流。

再是定理。
最大流最小割定理:最大流=最小割。由此可以得知最大流小于等于任意割的容量。

接下来我们就要看看这最大流应该怎么求呢?

增广路:如果图中的一条链满足以下要求:链中所有前向弧(与链方向相同的弧)都是非饱和弧,并且后向弧(与链方向相反的弧)都是非零的弧,那么该链就是关于可行流f的一条增广路。

如果一个可行流不是最大流,那么当前网络图中一定
还存在增广路。

增广:沿着增广路改进可行流的操作。

引入残余网络和残留容量。

残留容量:
对于每一条弧上的残留容量记 c f ( u , v ) = c ( u , v ) − f ( u , v ) cf(u,v)=c(u,v)-f(u,v) cf(u,v)=c(u,v)f(u,v),也就是这条弧上最多还能通过的流量。字面意思理解即可。当然还有个反向残留容量 c f ( v , u ) = − f ( u , v ) cf(v,u)=-f(u,v) cf(v,u)=f(u,v)

残余网络:
每找到一条源到汇的路径后将路径上的弧减去路径上最小的弧的容量,反向边上增加这个容量,得到的新图就是原图的残余网络。

要介绍的东西没了,下面就可以进入正题了。

费用流

费用流就是在网络流基础上给每条边都加上了费用cost,该网络的总费用就是 ∑ u , v ∈ E f u , v ∗ c o s t u , v \sum_{u,v\in E}f_{u,v}*cost_{u,v} u,vEfu,vcostu,v

下面就是喜闻乐见的算法了。

最大流算法

现在引入反向边。
反向边是干嘛的呢?你每次找到一条增广路,便直接增广了,想想便会发现问题,如果它卡了其他的路呢?答案就变小了,这个反向边就是给程序一个反悔的机会,而不是采用回溯,不然时间复杂度就直接上升到了一定境界。
那么反向边是怎么使用的呢?我们在找到一条增广路后,将路径上的边全部减去路径上的边的最小值,然后在这条边的反向边上加上这个值即可。

这里便直接讲解Dinic算法了。

Dinic算法

没错这便是今天的主题了。
Dinic算法将整张图按照路径长度分为了若干层,那么很显然,找的增广路便是满足所有的点在不同的层。

算法过程

  • 首先用BFS将图分层。
  • 如果发现到达不了汇点,程序就可以结束了,最大流已找到。
  • 之后便用DFS寻找增广路了(从一层走到另一层)。
  • 在过程中如果DFS到了汇点,则找到了一条增广路。进行增广,但是这个时候DFS并不会结束,而是回溯,继续找下一条路径。
  • 再执行如上操作。

下面看看它的时间复杂度。
普通情况下,复杂度为 O ( v 2 e ) \mathcal{O}(v^2e) O(v2e)

一些小优化。

  • 多路增广:每次不是寻找一条增广路,正如优化名字那样,形成一张增广网。
  • 当前弧优化:记录上一次检查到哪条边,如果找过,就不必再找他,找第一个没有找过的边。这为什么是对的呢?因为每次找增广路都会按照这条路径最大的能力增广,也就是说只要之前增广过的路一定是竭其所能的。

这边可以注意到,这些优化并没有优化时间复杂度,但是实际情况下可以快上不少。

一些方便的细节。

我们每次都要对反向弧操作,因此我们的边标号从0开始,这样每次只需要异或一下即可,方便快捷。

另外一说:网络流全部的难点在于建图,这意味着只会背模板也是可以接受的,所以优化在一开始就加足了,是很有帮助的。

下面附上我的模板代码QWQ,可能有点丑,能看就行了对吧。
模板题目地址。

#include 
using std::cin;
using std::cout;
using std::endl;
int S,T;
int n,m;
int head[200001],tot=1;
std::queue<int>q;
int deep[200001];
int cf[500001];
struct edge{
    int to;
    int nxt;
}e[500002];
void add(int x,int y,int z){
	e[++tot].to=y;
	e[tot].nxt=head[x];
    cf[tot]=z;
	head[x]=tot;
}
bool bfs(){
	memset(deep,0,sizeof(deep));
	while(!q.empty())
	    q.pop();
	q.push(S);
	deep[S]=1;
	while(!q.empty()){
		int now=q.front();
		q.pop();
		for(int i=head[now];i;i=e[i].nxt){
			int y=e[i].to;
			if(cf[i]&&!deep[y]){
				deep[y]=deep[now]+1;
				q.push(y);
			}
		}
	}
	return deep[T];
}
int dfs(int now,int min){
	if(now==T)
	    return min;
	for(int i=head[now];i;i=e[i].nxt){
		int y=e[i].to;
		if(deep[y]==deep[now]+1&&cf[i]){
			int w=dfs(y,std::min(min,cf[i]));
			if(w){
			    cf[i]-=w;
			    cf[i^1]+=w;
			    return w;
			}
		}
	}
	return 0;
}
int Dinic(){
	int ans=0;
	while(bfs()){
		while(int w=dfs(S,0x3f3f3f3f))
		    ans+=w;
	}
	return ans;
}
main(){
	std::ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>S>>T;
	for(int i=1;i<=m;++i){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
		add(y,x,0);
	}
	cout<<Dinic();
	return 0;
}

下面是加了当前弧优化的模板,其实两个优化同时加区别并不是很大。

#include 
using std::cin;
using std::cout;
using std::endl;
int S,T;
int n,m;
int head[200001],tot=1;
std::queue<int>q;
int deep[200001];
int cf[500001];
int last[500001];
struct edge{
    int to;
    int nxt;
}e[500002];
void add(int x,int y,int z){
	e[++tot].to=y;
	e[tot].nxt=head[x];
    cf[tot]=z;
	head[x]=tot;
}
bool bfs(){
	memset(deep,0,sizeof(deep));
	while(!q.empty())
	    q.pop();
	q.push(S);
	deep[S]=1;
	while(!q.empty()){
		int now=q.front();
		q.pop();
		for(int i=head[now];i;i=e[i].nxt){
			int y=e[i].to;
			if(cf[i]&&!deep[y]){
				deep[y]=deep[now]+1;
				q.push(y);
			}
		}
	}
	return deep[T];
}
int dfs(int now,int min){
	if(now==T)
	    return min;
	for(int &i=last[now];i;i=e[i].nxt){
		int y=e[i].to;
		if(deep[y]==deep[now]+1&&cf[i]){
			int w=dfs(y,std::min(min,cf[i]));
			if(w){
			    cf[i]-=w;
			    cf[i^1]+=w;
			    return w;
			}
		}
	}
	return 0;
}
int Dinic(){
	int ans=0;
	while(bfs()){
		for(int i=1;i<=n;++i)
		    last[i]=head[i];
		while(int w=dfs(S,0x3f3f3f3f))
		    ans+=w;
	}
	return ans;
}
main(){
	std::ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>S>>T; 
	for(int i=1;i<=m;++i){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
		add(y,x,0);
	}
	cout<<Dinic();
	return 0;
}

第二个模板:最小费用最大流。
直接上最短路,在最短路上增广即可。

#include 
int head[100001],tot=1;
int n,m;
int S,T;
int maxflow,mincost;
struct edge{
	int to;
	int nxt;
	int flow;
	int cost;
}e[200001];
int dis[100001];
int flow[100001];
int vis[100001];
int last[100001];
int Last[100001];
void add(int x,int y,int Flow,int Cost){
	e[++tot].to=y;
	e[tot].nxt=head[x];
	head[x]=tot;
	e[tot].flow=Flow;
	e[tot].cost=Cost;
}
bool spfa(){
	std::queue<int>q;
	memset(dis,0x3f,sizeof(dis));
	memset(flow,0x3f,sizeof(flow));
	memset(vis,0,sizeof(vis));
	while(!q.empty())
	    q.pop();
	q.push(S);
	vis[S]=1;
	dis[S]=0;
	Last[T]=0;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		vis[x]=0;
		for(int i=head[x];i;i=e[i].nxt){
			int y=e[i].to;
			if(e[i].flow&&dis[y]>dis[x]+e[i].cost){
			    dis[y]=dis[x]+e[i].cost;
			    Last[y]=x;
			    last[y]=i;
			    flow[y]=std::min(flow[x],e[i].flow);
			    if(!vis[y]){
			        vis[y]=1;
			        q.push(y);
			    }
			}
		}
	}
	return Last[T];
}
void mcmf(){
	while(spfa()){
		int now=T;
		maxflow+=flow[now];
		mincost+=dis[now]*flow[now];
		while(now!=S){
			e[last[now]].flow-=flow[T];
			e[last[now]^1].flow+=flow[T];
			now=Last[now];
		}
	}
}
main(){
	scanf("%d%d%d%d",&n,&m,&S,&T);
	for(int i=1;i<=m;++i){
	    int A,B,C,D;
	    scanf("%d%d%d%d",&A,&B,&C,&D);
	    add(A,B,C,D);
	    add(B,A,0,-D);
	}
	mcmf();
	printf("%d %d",maxflow,mincost);
	return 0;
}

你可能感兴趣的:(ZROI-2019.8.1)