ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【科技】 网络流学习笔记

2022-08-31 08:32:52  阅读:199  来源: 互联网

标签:en 增广 int MAX 网络 笔记 科技 edge long


网络瘤

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

一、几个定义

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\) 的路径,称为一条增广路。

二、最大流

这就是前言中所提到的那个问题了。

一个比较容易想到的思路是,不断地在残量网络中找寻增广路,直到没有增广路,此时的总流量即为最大流,但这个做法有点问题,例如下面这张图:

img

我们假设第一次增广,找到了 \(1->2->3->4\) 这条边,于是残量网络变成了这样:

img

这里做了个近似,我们直接把边的容量改为其残余容量。

此时已经无法继续增广了,算法结束,但不难发现,其实走 \(1->3->4\) 和 \(1->2->4\) 总流量为 \(2\),这更优。

那怎么办?

我们考虑给程序一个反悔的机会,也就是说,建立一种方法,使得已经流过了某条边的流量再流回去,也就是建立反向边,为了保持总容量不变,反向边初始容量为 \(0\)。

img

那么这时如果再走 \(1->2->3->4\),残量网络就变成了这样:

img

依然是为了保持总容量不变,在扣除正向边容量的同时,要给反向边加上相等的容量。

这时还可以继续增广:走 \(1->3->2->4\),惊奇的发现,\(2\) 给 \(3\) 的流量又让 \(3\) 给退回去了!而此时相当于选择了两条路径:\(1->3->4\) 和 \(1->2->4\),总流量为 \(2\),得到了正确的结果。

算法一、FF算法

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

#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1e5+10;
const int MOD=1e9+7;
inline char readchar() {
	static char buf[100000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}
inline int read() {
#define readchar getchar
	int res = 0, f = 0;
	char ch = readchar();
	for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
	for(; isdigit(ch);ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0');
	return f ? -res : res;
}
inline void write(int x) {
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int n,m,be,en;
struct node{int v,w,inv;};//由于是vector存图,所以需要整一个变量专门记录反向边 
vector<node> s[MAX];
int vis[MAX];
int dfs(int k=be,int flow=1e9)
{
	if(k==en) return flow;
	vis[k]=1;
	for(node &v:s[k])
	{
		int c; 
		if(v.w>0&&!vis[v.v]&&((c=dfs(v.v,min(v.w,flow)))!=-1))
		{
			v.w-=c;//本边剩余流量-c
			s[v.v][v.inv].w+=c;//反边流量+c
			return c;//找到增广路了
		}
	}
	return -1;//找不到增广路了,算法结束 
}
int FF()
{
	int ans=0,c;
	while((c=dfs())!=-1)
	{
		memset(vis,0,sizeof vis);
		ans+=c;
	}
	return ans; 
}
signed main()
{
	n=read(),m=read(),be=read(),en=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		s[u].push_back((node){v,w,(int)s[v].size()});//两边互为反向边 
		s[v].push_back((node){u,0,(int)s[u].size()-1});
	}
	cout<<FF();
	return 0;
}

该算法的复杂度上界为 \(O(ef)\) ,\(e\) 为边数,\(f\) 为最大流(艹我怎么知道怎么推的),慢的一匹,模板题都过不去:

考虑这个算法为啥这么慢,主要原因还是dfs好绕远路,每次找到的不是最短的增广路,所以复杂度没有保障。

你dfsT飞了你会想啥?

正常人应该都会想到bfs,

于是就有了EK算法。

算法二、EK算法

如上所述,EK就是bfs版的FF算法。

但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:

由于vector写EK很麻烦,于是我用了前向星。

//事实证明网络流还是用链式前向星吧 

#include<bits/stdc++.h>
#define ll long long
#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1.2e5+10;
const int MOD=1e9+7;
inline char readchar() {
	static char buf[100000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}
inline int read() {
#define readchar getchar
	int res = 0, f = 0;
	char ch = readchar();
	for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
	for(; isdigit(ch);ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0');
	return f ? -res : res;
}
inline void write(int x) {
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int n,m,be,en,cnt=1;
int vis[MAX],let[MAX],flow[MAX];
int head[MAX];
struct node{int net,to,w;}edge[MAX<<1];
void add(int u,int v,int w)
{
	edge[++cnt]=(node){head[u],v,w};
	head[u]=cnt;
	return ;
}
int bfs()
{
	memset(let,0,sizeof let);
	queue<int> q;
	q.push(be);
	flow[be]=1e9;
	while(!q.empty())
	{
		int ff=q.front();
		q.pop();
		if(ff==en) break;
		for(int i=head[ff];i;i=edge[i].net)
		{
			int v=edge[i].to,w=edge[i].w;
			if(w>0&&!let[v])
			{
				let[v]=i;
				flow[v]=min(flow[ff],w);
				q.push(v);
			}
		}
	}
	return let[en];
}
int EK()
{
	int mx=0;
	while(bfs())
	{
		mx+=flow[en];
		for(int i=en;i!=be;i=edge[let[i]^1].to)
		{
			edge[let[i]].w-=flow[en];
			edge[let[i]^1].w+=flow[en];
		}
	}
	return mx;
}
signed main()
{
	n=read(),m=read(),be=read(),en=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		add(u,v,w);add(v,u,0);
	}
	cout<<EK();
	return 0;
}

该算法复杂度上界为 \(O(ve^2)\),但我们都知到一般的网络流都是跑不满的,所以他能过模板题。

好像还跑的挺快(大误)?

但是本着精益求精防毒瘤出题人的精神,这个算法还得继续优化。

三、Dinic算法

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

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

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

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

#include<bits/stdc++.h>
#define ll long long
#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1e5+10;
const int MOD=1e9+7;
inline char readchar() {
	static char buf[100000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}
inline int read() {
#define readchar getchar
	int res = 0, f = 0;
	char ch = readchar();
	for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
	for(; isdigit(ch);ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0');
	return f ? -res : res;
}
inline void write(int x) {
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int n,m,be,en,cnt=1;
int vis[MAX],let[MAX],flow[MAX];
int head[MAX],dep[MAX],cur[MAX];
struct node{int net,to,w;}edge[MAX<<1];
void add(int u,int v,int w)
{
	edge[++cnt]=(node){head[u],v,w};
	head[u]=cnt;
	return ;
}
bool bfs()
{
	queue<int> q;
	memset(dep,0,sizeof dep);
	dep[be]=1;
	q.push(be);
	while(!q.empty())
	{
		int ff=q.front();q.pop();
		for(int i=head[ff];i;i=edge[i].net)
			if(!dep[edge[i].to]&&edge[i].w)
			{
				dep[edge[i].to]=dep[ff]+1;
				q.push(edge[i].to);
				if(edge[i].to==en) return 1;//分层完毕 
			}
	}
	return 0;//没搜到汇点,没有增广路了 
}
int dfs(int k,int mf)//mf代表当前节点现在能够供给的最大流量
{
	if(k==en) return mf;
	int sum=0;
	for(int i=cur[k];i;i=edge[i].net)//多路增广 
	{
		cur[k]=i;//当前弧优化,本次dfs的下次访问直接从当前弧开始 
		int v=edge[i].to,&w=edge[i].w;
		if(dep[v]==dep[k]+1&&w)//分层限制递归层数 
		{
			int f=dfs(v,min(mf,w));
			sum+=f;mf-=f;
			w-=f;edge[i^1].w+=f;
			if(mf==0) break;//该节点没有流量了,停止遍历 
		}
	}
	if(sum==0) dep[k]=0;//若该节点到不了汇点,将深度置为0,防止下次再次访问 
	return sum;
} 
int Dinic()
{
	int ans=0;
	while(bfs())
	{
		memcpy(cur,head,sizeof head);//把head弧拷贝到当前弧去
		ans+=dfs(be,1e9);
	}
	return ans;
}
signed main()
{
	n=read(),m=read(),be=read(),en=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		add(u,v,w);add(v,u,0);
	}
	cout<<Dinic();
	return 0;
}

优化效果很可观:

另外提一下,Dinic在二分图上的复杂度是 \(O(n\sqrt e)\),优于匈牙利算法。

此外,还有一些求最大流的算法,如ISAP,预流推进等,有兴趣可以了解一下(艹,谁会有兴趣)。

三、最小割

给一些定义:

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\) 割,

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

那么求最小割实际上就是求最大流,这里不再赘述。

四、费用流

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

回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,咋办?

最短路。

有负权咋办?

spfa。

但它不是死了吗?

怎么会有出题人在负权图上卡spfa

#include<bits/stdc++.h>
#define ll long long
#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1e5+10;
const int MOD=1e9+7;
inline char readchar() {
	static char buf[100000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}
inline int read() {
#define readchar getchar
	int res = 0, f = 0;
	char ch = readchar();
	for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
	for(; isdigit(ch);ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0');
	return f ? -res : res;
}
inline void write(int x) {
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int head[MAX];
struct node{int to,net,w,c;}edge[MAX<<1];
int n,m,be,en,cnt=1,let[MAX],dis[MAX],flow[MAX],vis[MAX];
void add(int u,int v,int w,int c)
{
	edge[++cnt]=(node){v,head[u],w,c};
	head[u]=cnt;
	return ;
}
bool spfa()
{
	memset(let,0,sizeof let);
	memset(flow,0,sizeof flow);
	memset(vis,0,sizeof vis);
	memset(dis,0x3f3f3f3f,sizeof dis);
	queue<int> q;q.push(be);
	dis[be]=0;vis[be]=1;flow[be]=1e9;
	while(!q.empty())
	{
		int ff=q.front();q.pop();
		vis[ff]=0;
//		if(ff==en) break;
		for(int i=head[ff];i;i=edge[i].net)
		{
			int v=edge[i].to,c=edge[i].c,w=edge[i].w;
			if(dis[ff]+c<dis[v]&&w)//可松弛且残流不为0
			{
				let[v]=i;
				dis[v]=dis[ff]+c;
				flow[v]=min(w,flow[ff]);
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}
	return flow[en];
}
int ans1=0,ans2=0;
void EK()
{
	while(spfa())
	{
		ans1+=flow[en];
		ans2+=flow[en]*dis[en];
		for(int i=en;i!=be;i=edge[let[i]^1].to)
		{
			edge[let[i]].w-=flow[en];
			edge[let[i]^1].w+=flow[en];
		}
	}
	return ;
}

signed main()
{
	n=read(),m=read(),be=read(),en=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read(),c=read();
		add(u,v,w,c);add(v,u,0,-c);
	}
	EK();
	cout<<ans1<<" "<<ans2;
	return 0;
}

当然dijkstra也存在一种方法来处理负权图,但这超出了我们的讨论范围以及我的认知水平

五、一点例题

鲁迅曾说过:网络流最难的是建图。我们通过几道例题来看一下究竟该怎么考虑。

P2756 飞行员配对方案问题

P1129 ZJOI2007 矩阵游戏

标签:en,增广,int,MAX,网络,笔记,科技,edge,long
来源: https://www.cnblogs.com/wapmhac/p/16641647.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有