首页 > 其他分享 >【科技】 网络流学习笔记

【科技】 网络流学习笔记

时间:2022-08-31 08:22:06浏览次数:75  
标签: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
From: https://www.cnblogs.com/wapmhac/p/16641647.html

相关文章

  • MySQL刷题复习笔记 - 每日持续更新
    PS为了代码规范,所以所有关键字均为大写,其他为小写。点击题目名称即为题解链接。MySQL基本语法SELECT[DISTINCT]列名1,列名2...FROM表名WHERE查询条件表达......
  • 【学习笔记】CSS 动画keyframes
    【学习笔记】CSS动画keyframes必要项目@keyframes动画名称对应animation-name:动画名称动画持续时间,指动画开始到结束时间,预设为0,若没有设定,动画不会执行。下......
  • QT网络编程【二】【Socket】
    1.QT中添加socket库的相关操作2.正常c++11VS2019使用socket库的操作3.winsock2与sys/socket.h的区别?4.WinSock2的基本操作?5.socket的创建参数的说明[]:http://t......
  • Linux操作系统中通过命令操作Oracle数据库--笔记大全
    1.Windowsserver服务器安装数据库忘记对某个用户解锁,比如Scott,我们可以通过system用户来对该用户解锁:步骤如下:注:sys/system/oracle数据库用户都是管理员用户(1)在运行中输......
  • Spring学习笔记(六)——AOP
    1.AOP简介1.1AOPSpring框架的一个关键组件是面向切面的编程(AOP)框架。面向切面的编程需要把程序逻辑分解成不同的部分称为所谓的关注点。跨一个应用程序的多个点的功......
  • 2022-08-30 第四小组 王星苹 学习笔记
    学习心得在浏览器禁用cookie的情况下,仍可以用于会话管理机制的是HTTPSession。重定向时,浏览器中的地址栏url会发生变化,重定向调用的是HttpServletResponse对象中的方法......
  • 第四章 网络安全体系与网络安全模型
    1、网络安全体系特征(1)整体性:网络安全体系从全局、长远的角度实现安全保障,网络安全单元按照一定的规则,相互依赖、相互约束、相互作用而形成人机物一体化的网络安全保护......
  • 云聚华为伙伴暨开发者大会GaussDB专场,与客户伙伴共话金融科技新发
     近日,2022年华为伙伴暨开发者大会正在以线上的方式火热进行,会上集结了众多行业专家、合作伙伴和客户,一起共话行业前沿趋势,探索技术新发展。在“探秘GaussDB,打造坚实金融......
  • esp32的esp_wifi(wifi驱动库),esp_netif(网络接口) ,lwip(轻量级TCP/IP网络协议栈)是什
    .esp32的esp_wifi(wifi驱动库),esp_netif(网络接口),lwip(轻量级TCP/IP网络协议栈)是什么?三者之间有什么关系?esp_wifi驱动库用户控制wifi硬件单元;lwip是一层纯软件,轻量级......
  • 2022-08-29 第二小组 张鑫 学习笔记
    实训五十一天JavaWEB学习内容事件修饰符用来和事件连用的,决定事件触发条件或者阻止事件的触发机制事件的冒泡点击div里的按钮,div被点击的事件也会被触发.stop修饰......