//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

网络流学习笔记

咕咕咕
此博客参考学长的课件,代码是直接粘过来的,所以不要觉得奇怪。

网络

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

一、几个定义

1. 网络

对于有向图\(G=(V,E)\),其中每条边\((u,v) \in E\)都有权值\(w_{ij}\),称之为容量,图中有两个特殊的点\(s,t(s\ne 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. \(f(u,v)=-f(v,u)\)即一条边的流量与其反向边的流量互为相反数。
  3. \(\forall x\in E- \left \{s,t\right\},\sum_{(u,v)\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\)的路径,称为一条增广路。

二、最大流

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

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

这里做了个近似,我们直接把边的流量改为其残余容量。
此时已经无法继续增广了,算法结束,但不难发现,其实走\(1->3->4\)\(1->2->4\)总流量为\(2\),这更优。
那怎么办?
我们考虑给程序一个反悔的机会,也就是说,建立一种方法,使得已经流过了某条边的流量再流回去,也就是建立反向边,为了保持总容量不变,反向边初始容量为\(0\)

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

依然是为了保持总容量不变,在扣除正向边容量的同时,要给反向边加上相等的容量。
这时还可以继续增广:走\(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;
}

这个算法就是慢,板子题都过不去。
考虑这个算法为啥这么慢,主要原因还是dfs好绕远路,每次找到的不是最短的增广路,所以复杂度没有保障。
你dfs T 飞了你会想啥?
正常人应该都会想到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;
}

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

Dinic算法

然而,最常用的网络流算法是Dinic算法。作为FF/EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是\(O(v^{2}e)\) 。所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为\(0\)不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
这份代码是我自己写的。
模板题

#include<bits/stdc++.h>
#define int long long
#define NN 200010
#define N 10010
using namespace std;
int n,m,s,t,ans=0,cnt=1,q[NN],l,r;//cnt存放边的数量,ans存放答案,q用于模拟队列 
int head[N],nxt[NN],to[NN],val[NN],vis[N];//vis标记此点的深度,存边
inline void add(int u,int v,int w)//建边 
{
	to[++cnt]=v;//终点 
	val[cnt]=w;//当前边的最大流量 
	nxt[cnt]=head[u];//赋值头节点 
	head[u]=cnt;//更新下一个的头节点 
}
int bfs()//bfs 
{
	memset(vis,0,sizeof(vis));//清空vis数组 
	q[l=r=1]=s;//赋初值 
	vis[s]=1;//标记不能走了 
	while(l<=r)//只要没到汇点就一直循环找 
	{
		int u=q[l++];//取出队头元素 
		for(int p=head[u];p;p=nxt[p])//循环每一条与此点相连的点 
		{
			int v=to[p];//取出终点 
			if(val[p]&&!vis[v])//如果当前点没有走过并且当此边是有剩余的流量的时候 
			{
				vis[v]=vis[u]+1;//计算当前点的深度 
				q[++r]=v;//入列 
			}
		}
	}
	return vis[t];//返回汇点的深度 
}
int dfs(int u,int in)//u是起点,in是从上一条边流进的流量 
{
	if(u==t)//如果当前的u到达了汇点 
	  return in;//就直接返回当前点的进来的流量 
	int out=0;//out表示从当前边流出的流量 
	for(int p=head[u];p&&in;p=nxt[p])//一个一个遍历能够到达的点并且保证流入的流量不为0 
	{
		int v=to[p];//取出终点 
		if(val[p]&&vis[v]==vis[u]+1)//如果当前点的最大流量不为0并且深度是起点加一(保证向汇点进行深搜) 
		{
			int res=dfs(v,min(val[p],in));//res是当前点的内流向下一条边的最大流量 
			val[p]-=res;//正向边减去 
			val[p^1]+=res;//反向边加上 
			in-=res;//减去 
			out+=res;//加上 
		}
	}
	if(out==0)//如果流出的为0 
	  vis[u]=0;//标记不能到达汇点下一次不搜了 
	return out;//返回到达汇点的流量 
}
signed main()
{
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w);//存正向边 
		add(v,u,0);//存反向边 
	}
	while(bfs())//只要有能到达汇点的路线 
	  ans+=dfs(s,1e18);//累加答案 
	cout<<ans<<endl;//输出 
	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\)割,
    所以最大流\(=\)最小割。
    那么求最小割实际上就是求最大流,这里不在赘述。

四、费用流

我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用\(c(u,v)\) ,于是为了惠民,自来水厂想找到一种方法,使得流最大的同时费用最小,这就是最小费用最大流。
回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,咋办?
最短路。
有负权咋办?
SPFA他活了。
因为有模板题所以这道题代码也是我自己写的。

#include<bits/stdc++.h>
#define INF 0x7fffffff
using namespace std;
queue<int> q;
int head[5001],cost[100001],net[100001],to[100001],val[100001];//cost为费用数组,val为容量
int cnt=1,n,m,xb[5001];//记录下标,便于修改容量
int flow[5001],pre[5001];//前驱节点
int mflow=0,mcost=0;//最大流最小费用
int dis[5001],f[5001];//记录从源点到当前节点的最小的费用值,标记是否在队列中
void add(int x,int y,int c,int z)//建边 
{
	to[++cnt]=y;//存终点 
	cost[cnt]=z;//存花费 
	val[cnt]=c;//存边的最大流量 
	net[cnt]=head[x];
	head[x]=cnt;
}
int BFS(int s,int t)//bfs 
{
	memset(dis,127,sizeof(dis));//重置dis数组 
	memset(f,0,sizeof(f));//清空f数组 
	int inf=dis[0];//给inf赋初值 
	while(!q.empty())//把栈清空 
		q.pop();
	for(int i=1; i<=n; i++)//把前驱都赋成-1 
		pre[i]=-1;
	f[s]=1;//标记起点入列 
	dis[s]=0;//原点到当前节点的最小费用为0 
	pre[s]=0;//没有前驱 
	flow[s]=INF;//起点的 
	q.push(s);//起点入列 
	while(!q.empty())//只要栈不空 
	{
		int u=q.front();//取出队头元素 
		q.pop();//弹出 
		f[u]=0;//标记出列 
		for(int i=head[u]; i; i=net[i])
		{
			int v=to[i];//取出终点 
			if(val[i]>0&&dis[v]>dis[u]+cost[i])//如果当前边的最大流量不为0并且加上当前点的花费比原来要小 
			{
				dis[v]=dis[u]+cost[i];//更新花费 
				pre[v]=u;//更新前驱 
				xb[v]=i;//存下标 
				flow[v]=min(flow[u],val[i]);//更新最大流 
				if(!f[v]) f[v]=1,q.push(v);//没入列就入列 
			}
		}
	}
	if(dis[t]>=inf) return 0;//如果比起点大就返回0 
	return 1;//否则返回1 
}
void max_flow(int s,int t)//算最大值 
{
	while(BFS(s,t))//只要还能到汇点 
	{
		int k=t;//存终点 
		while(k!=s)
		{
			val[xb[k]]-=flow[t];//减去 
			val[xb[k]^1]+=flow[t];//加上 
			k=pre[k];//更新k值 
		}
		mflow+=flow[t];//最大流量 
		mcost+=flow[t]*dis[t];//最小花费 
	}
}
int main()
{
	int s,t;
	cin>>n>>m>>s>>t;
	for(int i=1; i<=m; i++)
	{
		int x,y,c,d;
		cin>>x>>y>>c>>d;
		add(x,y,c,d);//建边 
		add(y,x,0,-d);
	}
	max_flow(s,t);
	cout<<mflow<<" "<<mcost<<endl;//输出
	return 0; 
}

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

如果你觉得你行了

代码及注释因为我太懒了就不写解析了咕咕咕:

#include<bits/stdc++.h>
#define inf 1<<30 
using namespace std;
int n,p,q,s,t,vi,m_in;
struct sb{int v,val,next;} e[101101];//存放已经建好的边 
int head[1010],cnt=1,vis[1010],dep[1010];//head是头节点,cnt是边的数量,vis标记此点是否入列,dep表示深度 
inline void add(int u,int v,int val)//建边函数 
{
	e[++cnt].v=v;//存终点 
	e[cnt].val=val;//存每一条边的最大流量 
	e[cnt].next=head[u];//头节点 
	head[u]=cnt;//更新 
}
int bfs()//bfs 
{
	memset(vis,0,sizeof(vis));//清空vis数组 
	memset(dep,0x3f,sizeof(dep));//重置dep数组 
	queue<int>q;//定义队列用于bfs 
	q.push(s);//放入队列 
	dep[s]=1;//标记出列 
	while(!q.empty())//只要队列不空 
	{
		int u=q.front();//取出队头元素 
		q.pop();//弹出队头元素 
		vis[u]=0;//标记出列 
		for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边 
		{
			int v=e[i].v;//取出终点 
			if(e[i].val&&dep[v]>dep[u]+1)//如果当前边最大流量不为0并且深度比从起点到终点大 
			{
				dep[v]=dep[u]+1;//替换 
				if(vis[v]==0)//如果不在队列里面 
				{
					q.push(v);//放入队列 
					vis[v]=1;//标记入列 
				}
			}
		}
	}
	return dep[t]!=0x3f3f3f3f;//如果能到达汇点返回1,反之返回0 
}
int dfs(int u,int in)//dfs函数 
{
	if(u==t)//到达汇点了 
	{
		vi=1;//标记找到了 
		return in;//返回当前路线的流量 
	}
	int out=0;//流出的流量大小 
	for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边 
	{
		int v=e[i].v;//取出终点 
		if(e[i].val&&dep[v]==dep[u]+1)//如果当前点最大流量不为0并且是向汇点流去 
		{
			int res=dfs(v,min(e[i].val,in));//递归找此边的流量大小 
			e[i].val-=res;//正向边加上 
			e[i^1].val+=res;//反向边减去 
			out+=res;//累加流出的流量大小 
		}
		if(out==in)break;//如果当前点流出和流入的量相等就直接退出 
	}
	if(out==0)//如果当前的流出的流量等于0 
	  vis[u]=0; //标记下一次不搜了 
	return out;//返回流量大小 
}
int main()
{
	cin>>n>>p>>q;
	int f;
	s=1001,t=1002;//原点,汇点 
	for(int i=1;i<=n;i++)add(i,i+n,1),add(i+n,i,0);//i表示顾客入点,i+n表示顾客出点,自己与自己建边 
	for(int i=1;i<=p;i++)add(s,200+i,1),add(200+i,s,0); //200+i表示房间,与原点相连建边 
	for(int i=1;i<=q;i++)add(300+i,t,1),add(t,300+i,0); //300+i表示菜,与汇点相连建边 
	for(int i=1;i<=n;i++)//枚举每一个顾客 
		for(int j=1;j<=p;j++)//枚举每一个房间 
		{
			cin>>f;//输入 
			if(f==1)//喜欢此房间 
			  add(200+j,i,1),add(i,200+j,0);//当前顾客与房间建边 
		}
	for(int i=1;i<=n;i++)//枚举每一个顾客 
		for(int j=1;j<=q;j++)//枚举每一道菜 
		{
			cin>>f;
			if(f==1)//喜欢这道菜 
			  add(i+n,300+j,1),add(300+j,i+n,0);//之前与自己建的边与菜建边 
		}
	while(bfs())//只要还能到汇点 
	{
		vi=1;
		while(vi)vi=0,m_in+=dfs(s,inf);//累加答案
	}
	cout<<m_in<<endl;//输出 
	return 0;//好习惯 
}
posted @ 2022-10-05 20:04  北烛青澜  阅读(59)  评论(0编辑  收藏  举报