网络流

前言:关于网络流有个生动的比喻,想象一个自来水厂向各处供水,自来水厂有无限多的水,但每条管子单位时间内允许的最大流量有限,现在钦定一个出水口为汇点,现在要做的就是在满足每一条管子不爆的情况下,最大化汇点流出的水量。

定义

1.网络
对于有向图 \(G = (V,E)\) ,其中每条边 \((u,v) \in E\) 都有权值 \(w(u,v)\) ,称之为容量,图中有两个特殊的点 \(s\) , \(t\) \((s\not= t)\) ,称 \(s\) 为源点,\(t\) 为汇点,这个图称为网络。

2.流
对于任意的 \((u,v) \in E\) ,称 \(f(u,v)\)\((u,v)\) 边的流量,\(f(u,v)\) 恒满足:

(1) \(f(u,v)\le w(u,v)\) ,即一条边的流量不能超过其容量。

(2) 若 \((v,u) \in E\) ,则 \(f(u,v)=-f(v,u)\) ,即一条边的流量与其相反边的流量互为相反
数(注意不是反向边,反向边与相反边的区别是反向边 \((v,u) \notin E\) )。

(3) \(\forall x \in E -\) {\(s,t\)} , \(\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\),即流入一个点的流量等于流
出这个点的流量。

3.残量网络

对于所有的 \(w(u,v)-f(u,v)>0\) 的边组成的网络,称其为残量网络,残量网络中的边可能不属于\(E\)

4.增广路

在原图 \(G\) 或其某一个残量网络中,一条每条边的剩余容量都大于0 的从\(s\)\(t\) 的路
径,称为一条增广路。

最大流

FF算法

最暴力的最大流算法,每次直接dfs找增广路,找不到了就完成。

ee,因为跑得太慢了当时学了接着就忘了。

EK算法

如上所述,EK就是bfs版的FF算法。
但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:

int bfs(){
	memset(let,0,sizeof(let));
	queue<int>q;
	q.push(be);
	flow[be]=1e9;
	while(!q.empty()){
		int now=q.front();q.pop();
		if(now==en) break;
		for(int i=head[now];i;i=a[i].next){
			int v=a[i].v,w=a[i].w;
			if(w>0&&!let[v]){
				let[v]=i;
				flow[v]=min(flow[now],w);
				q.push(v);
			}
		}
	}
	return let[en];
}

int EK(){
	int ans=0;
	while(bfs()){
		ans+=flow[en];
		for(int i=en;i!=be;i=a[let[i]^1].v){
			a[let[i]].w-=flow[en];
			a[let[i]^1].w+=flow[en];
		}
	}
	return ans;
}
signed main()

{
	n=read();m=read();be=read();en=read();
	for(int i=1;i<=m;i++){
		x=read();y=read();z=read();
		add(x,y,z);add(y,x,0);
	}
	cout<<EK();
	return 0;
}

Dinic算法

然而,最常用的网络流算法是Dinic算法。作为FF/EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是 。

所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为 不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。

我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。

此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。

code:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10; 
int n,m,be,en,cnt=1,head[N],cur[N],dep[N];//找相反边的一个小技巧
struct node{
	int u,v,w,nxt;
}a[N];
void add(int u,int v,int w){
	a[++cnt]=(node){u,v,w,head[u]};
	head[u]=cnt;
}
inline int read(){char ch=getchar();int f=0,x=0;
	while(ch<'0'||ch>'9'){if(ch=='-') f=1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return f?-x:x;
}
int bfs(){//分层
	memset(dep,0,sizeof(dep));
	queue<int>q;
	q.push(be);
	dep[be]=1;
	while(!q.empty()){
		int now=q.front();
		q.pop();
		for(int i=head[now];i;i=a[i].nxt){
			if(!dep[a[i].v]&&a[i].w){
				dep[a[i].v]=dep[now]+1;
				q.push(a[i].v);
				if(a[i].v==en) return 1;//有增广路且分层完毕
			}
		}
	}
	return 0;//没搜到汇点,没增广路了
}
int dfs(int k,int flw){//寻找
	if(k==en) return flw;//当前能提供的最大流量
	int sum=0;
	for(int i=cur[k];i;i=a[i].nxt){//多路增广
		cur[k]=i;//当前弧优化,本次dfs的下次访问直接从当前弧开始
		int v=a[i].v,&w=a[i].w;
		if(dep[v]==dep[k]+1&&w){//分层限制层数
			int f=dfs(v,min(w,flw));
			sum+=f;flw-=f;
			a[i^1].w+=f;w-=f;
			if(flw==0) break;//没有流量了,停止遍历
		}
	}
	if(sum==0) dep[k]=0;
	return sum;
}
int Dinic(){
	int ans=0;
	while(bfs()){
		memcpy(cur,head,sizeof(head));//head -> cur
		ans+=dfs(be,1e9); 
	}
	return ans;
}
signed main()
{
	n=read();m=read();be=read();en=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),z=read();
		add(x,y,z);
		add(y,x,0);
	}
	cout<<Dinic();
	return 0;
}

最小割

定义:
1.割:对于网络 \(G\) ,其割代表一种点的划分方式,这种划分方式需要满足将 \(G\) 恰好分
为两部分 \(S,T\) ,且 \(s\in S\)\(t \in T\)

2.割的容量:表示所有的从 \(S\)\(T\) 的边的容量之和,即:\(w(S,T)=\sum_{u\in S,v\in T}w(u,v)\)

3.最小割:容量最小的割即为最小割。

如何求最小割?

这里有一条定理,极其简洁的解决了这个问题:
最大流 \(=\) 最小割。

证明:

可以把最小割认为是将一些边割断,使得整个图分为 \(S\)\(T\) 两部分,那么容易得到图中所有的流量必定流经这些边中的某一条(否则无法从 \(s\) 到达 \(t\) ),所以这些边的总流量 \(=\) 图的总流量。

而边的流量 \(\le\) 边的容量,

所以这些边的总流量 \(\le\)这些边的总容量,

所以图的总流量\(\le\) 这些边的总容量 ,

所以流 \(\le\) 割,所以最大流 \(=\) 最小割。

费用流

我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用 ,但是为了惠民,自来水厂想找到一种方法,使得流最大的同时费用最小,这就是最小费用最大流。

回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,可以跑最短路,有负权就用spfa,可以判负环。

最小费用最大流

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m,be,en,cnt=1,ans1,ans2,head[N],vis[N],dis[N],pre[N],flow[N];
struct node{
	int u,v,w,c,nxt;
}a[N];
void add(int u,int v,int w,int c){
	a[++cnt]=(node){u,v,w,c,head[u]};
	head[u]=cnt;
}
inline int read(){
	char ch=getchar();int f=0,x=0;
	while(ch<'0'||ch>'9'){if(ch=='-') f=1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return f?-x:x;
}
int spfa(){
	memset(dis,0x3f3f3f3f,sizeof(dis));
	memset(pre,0,sizeof(pre));
	memset(vis,0,sizeof(vis));
	memset(flow,0,sizeof(flow));
	dis[be]=0;vis[be]=1;flow[be]=1e9;
	queue<int>q;
	q.push(be);
	while(!q.empty()){
		int now=q.front();
		q.pop();
		vis[now]=0;
		for(int i=head[now];i;i=a[i].nxt){
			int v=a[i].v,w=a[i].w,c=a[i].c;
			if(dis[v]>dis[now]+c&&w){
				dis[v]=dis[now]+c;
				pre[v]=i;
				flow[v]=min(flow[now],w);
				if(!vis[v]){
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}
	return flow[en];
}
void Dinic(){
	while(spfa()){
		ans1+=flow[en];
		ans2+=flow[en]*dis[en];
		for(int i=en;i!=be;i=a[pre[i]^1].v){
			a[pre[i]].w-=flow[en];
			a[pre[i]^1].w+=flow[en];
		}
	}
	return ;
}
signed main()
{
	n=read();m=read();be=read();en=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),w=read(),c=read();
		add(x,y,w,c);add(y,x,0,-c);
	}
	Dinic();
	cout<<ans1<<" "<<ans2<<endl;
	return 0;
}

对于费用流来讲,还存在有负环的情况,负环跑spfa就炸了,所以就要用消圈算法(待补)。

posted @ 2023-06-19 19:35  XYini  阅读(2)  评论(0)    收藏  举报