网络流——从入门到入土

1. 网络流的基本概念

注: \(G\) 为流网络, \(f\) 为可行流, \(|f|\) 为可行流的大小。

1. 流网络

一个有向图 \(G=(V,E)\) ,每一条边都有流量限制 \(C(u,v)\) ,源点为 \(s\) ,汇点为 \(t\)

2. 可行流

设一个可行流为 \(f\) ,它需要满足两个限制:

1.容量限制:

\(\forall (u,v)\in E,0\leqslant f(u,v)\leqslant C(u,v)\)

对于每一条边,它的流量一定是大于 \(0\) 小于流量限制的。

2.流量守恒:

\(\forall x\in V|\{s,t\},\sum\limits_{(u,x)\in E}f(u,x)=\sum\limits_{(x,v)\in E}f(x,v)\)

对于每一个点(除了源点和汇点),它收到的流量等于输出的流量。

3. 可行流的大小

\(|f|=\sum\limits_{(s,v)\in E}f(s,v)-\sum\limits_{(v,s)\in E}f(v,s)\)

可行流的大小即为源点输出的流量减去收到的流量(一般没有连向源点的边)。

4. 最大流

最大流,全称为最大可行流

即为流网络中大小最大的可行流。

5. 残留网络

对于每一个可行流 \(f\) ,都有一个残留网络 \(G\) ,且可行流与残留网络是一一对应的。

一般记为 \(G_f\)

1.性质

\(V_f=V\)

残留网络包含原图中的所有点。

\(E_f=E+E'\)

残留网络包含原图中的所有边以及反向边。

\(C'(u,v)\) 为残留网络中每条边的容量。

\(C'(u,v)=\begin{cases}C(u,v)-f(u,v)&(u,v)\in E\\f(v,u)&(v,u)\in E\end{cases}\)

若当前边为正向边,容量即为原容量减去现在的流量,否则即为现在的流量。

\(G_f\) 的可行流为 \(f'\)

\(f+f'\) 也是 \(G\) 的一个可行流。

2. 流网络的相加

\(\forall (u,v) \in E,f(u,v)\gets\begin{cases}f(u,v)+f'(u,v)&(u,v)\in E'\\f(u,v)-f'(v,u)&(v,u)\in E\end{cases}\)

对于每一条边,若另一个流网络中存在与它相同方向的边,则将两流量相加,否则相减。

\(f+f'\)\(G\) 的一个可行流,那么它需要满足可行流的两个条件。

1.容量限制

若对于 \((u,v)\in E\) ,有 \((u,v)\in E'\)

\(\Rightarrow 0\leqslant f'(u,v)\leqslant C'(u,v)=C(u,v)-f(u,v)\)

\(\Rightarrow 0\leqslant f'(u,v)\leqslant C(u,v)-f(u,v)\)

\(\Rightarrow 0\leqslant\color{red}{f'(u,v)+f(u,v)\leqslant C(u,v)}\)

若对于 \((u,v)\in E\) ,有 \((v,u)\in E'\)

\(\Rightarrow 0 \leqslant f'(u,v)\leqslant C'(u,v)=f(v,u)\leqslant C(v,u)\)

\(\Rightarrow 0 \leqslant\color{red}{f(v,u)-f'(u,v) \leqslant C(u,v)}\)

至此,可证明 \(f+f'\) 满足容量限制。

2.流量守恒

对于 \(f\) 中的所有点,都满足流量守恒。

\(f'\) 同样满足,而相加不改变守恒。

易得 \(f+f'\) 满足流量守恒。

可得 \(|f+f'|=|f|+|f'|\)

所以残留网络中的可行流是可以加到流网络的可行流中的。

可以推出如果残留网络没有可行流,流网络一定是最大流。

6. 增广路

在残留网络里面,从源点出发,沿着容量大于 \(0\) 的边走,如果可以走到汇点,那么这条边是增广路。

增广路是无环无交点的简单路径。

对于一个可行流 \(f\) ,若残留网络 \(G_f\) 中没有增广路,那么 \(f\) 是最大流。

7. 流网络的割

一般的,设流网络的两个点集为 \(X\)\(Y\)

可得 \(f(X,Y)=\sum\limits_{u\in X}\sum\limits_{v\in Y}f(u,v)-\sum\limits_{u\in Y}\sum\limits_{v\in X}f(u,v)\)

性质一: \(f(X,Y)=-f(Y,X)\)

性质二: \(f(X,X)=0\)

性质三: \(f(Z,X\bigcup Y)=f(Z,X)+f(Z,Y)\{X\bigcup Y=\varnothing\}\)

对于流网络 \(G=(V,E)\) ,将 \(V\) 分为 \(S\)\(T\)

满足 \(S\bigcup T=V\)\(S\bigcap T=\varnothing\)\(s\in S\)\(t\in T\)

1. 割的容量

\(C(S,T)=\sum\limits_{u\in S}\sum\limits_{v\in T}C(u,v)\)

割的容量为从 \(S\)\(T\) 的边的容量之和。

最小割指的是容量最小的割

2. 割的流量

\(f(S,T)=\sum\limits_{u\in S}\sum\limits_{v\in T}f(u,v)-\sum\limits_{u\in T}\sum\limits_{v\in S}f(u,v)\)

割的流量为从 \(S\)\(T\) 的边的流量之和减去从 \(T\)\(S\) 的边的流量之和。

3. 割的性质

\(\forall \left[S,T\right],\forall f,f(S,T)\leqslant C(S,T)\)

对于每一个割和每一个可行流,割的流量小于等于割的容量。

\(f(S,T)=\sum\limits_{u\in S}\sum\limits_{v\in T}f(u,v)-\sum\limits_{u\in T}\sum\limits_{v\in S}f(u,v)\)

\(\Rightarrow f(S,T)\leqslant\sum\limits_{u\in S}\sum\limits_{v\in T}f(u,v)\)

\(\because \sum\limits_{u\in S}\sum\limits_{v\in T}f(u,v)\leqslant \sum\limits_{u\in S}\sum\limits_{v\in T}C(u,v)=C(S,T)\)

\(\Rightarrow \color{red}{f(S,T)\leqslant C(S,T)}\)

\(\forall [S,T],\forall f,f(S,T)=|f|\)

对于每一个割和每一个可行流,割的容量等于可行流的大小。

\(f(S,V)=f(S,S)+f(S,T)\) (性质一)

\(\Rightarrow f(S,T)=f(S,V)-f(S,S)\)

\(\Rightarrow f(S,T)=f(S,V)\)

\(\Rightarrow f(S,T)=f(\{s\},V)+f(S-\{s\},V)\)

\(S'=S-\{s\}\)

\(\because f(S',V)=\sum\limits_{u\in S'}\sum\limits_{v\in V}f(u,v)-\sum\limits_{u\in S'}\sum\limits_{v\in V}f(v,u)\)

\(\Rightarrow f(S',V)=\sum\limits_{u\in S'}\big(\sum\limits_{v\in V}f(u,v)-\sum\limits_{v\in V}f(v,u)\big)=0\)

\(\therefore f(S,T)=f(s,V)=|f|\)

\(\Rightarrow \color{red}{f(S,T)=|f|}\)

综上可以推出:\(|f|\leqslant C(S,T)\)

即为最大流\(\leqslant\)最小割

8. 最大流最小割定理

对于流网络 \(G=(V,E)\) ,同时满足下面三个条件。

1. \(f\) 是最大流

2. \(G_f\) 中不存在增广路

3. \(\exists \left[S,T\right],|f|=C(S,T)\)

\(1\Rightarrow2\)

\(f\) 为当前可行流,若存在增广路 \(f'\) ,那么 \(|f+f'|\geqslant|f|\)

\(2\Rightarrow3\)

\(G_f\) 中从 \(s\) 出发沿着容量大于 \(0\) 的边走到的所有的点均在 \(S\) 中。

\(T=V-S\)

\(\forall (x,y)\{x\in S,y\in T\},f(x,y)=C(x,y)\)

\(\forall (a,b)\{a\in T,b\in S\},f(a,b)=0\)

\(\because |f|=f(S,T)\)

\(\Rightarrow |f|=\sum\limits_{u\in S}\sum\limits_{v\in T}f(u,v)-\sum\limits_{u\in S}\sum\limits_{v\in T}f(v,u)\)

\(\Rightarrow |f|=\sum\limits_{u\in S}\sum\limits_{v\in T}C(u,v)=C(S,T)\)

\(3\Rightarrow1\)

\(|f|=C(S,T)\geqslant|f_{max}|\)

\(\because |f|=|f_{max}|\)

\(\therefore |f|=C(S,T)\)

最大流等于最小割

2. 网络流的模板

1. FF

因为最大流中没有增广路,所以可以通过不断的找增广路来找到原图的最大流。

这就是 FF 思想,也是一堆最大流算法的根源。

2. EK

通过暴力找增广路,然后加入残留网络,这是 EK 的思想。

大概分为以下几步:

  1. 通过 BFS 找到一条增广路 \(f'\)
  2. 更新残留网络,若对于一条边 \((u,v)\) 流过了 \(k\) 的流量,那么 \(\begin{cases}C(u,v)=C(u,v)-k\\C(v,u)=C(v,u)+k\end{cases}\)
  3. 需要减的流量为找到增广路上最小的容量

肯定用邻接表存图,所以有一个小 trick : \(i\) 的反向边为 \(i \oplus 1\)

时间复杂度不优,为 \(O(nm^2)\)

但是跑不满\(10^3\)\(10^4\) 可跑。

最大流一般不用,但是费用流要用。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
struct node
{
    int to,nxt,flow;
}e[MAXN];
int head[MAXN],cnt=1;//因为要异或得反向边,cnt需从1开始
int n,m,s,t;
inline void add(int x,int y,int f)
{
    e[++cnt].to=y;
    e[cnt].flow=f;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}
bitset<MAXN>vis;
int pre[MAXN],dis[MAXN];//pre记录当前点是从哪条边过来的,dis是当前点之前最小的容量
inline bool bfs()//bfs找一条增广路
{
    vis=0;
    vis[s]=1;
    dis[s]=0x7f7f7f7f;
    queue<int>q;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(register int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to,f=e[i].flow;
            if(!vis[y]&&f)//若没有经过且容量大于0,则可以成为增广路的一部分
            {
                pre[y]=i;//直接记录边的编号
                dis[y]=min(dis[x],f);
                vis[y]=1;
                if(y==t)return true;//现在到了汇点,直接找到了
                q.push(y);
            }
        }
    }
    return false;
}
inline void EK()
{
    int ans=0;
    while(bfs())
    {
        ans+=dis[t];
        for(register int i=t;i!=s;i=e[pre[i]^1].to)//不断遍历直到访问到源点
        {
            e[pre[i]].flow-=dis[t];//流过的边容量要减
            e[pre[i]^1].flow+=dis[t];//小trick应用处
        }
    }
    printf("%d",ans);
}
int main()
{
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(register int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        add(y,x,0);//反向边最初容量为0
    }
    EK();
    return 0;
}

3. dinic

思想类似于EK,但是一次可以找到多条增广路 。

通过暴搜来找到当前残留网络中的所有增广路。

但是需要注意的是,如果残留网络中有环,这个算法就寄了。

所以需要引进一个叫做分层图的概念,即只能从层数低的走向层数高的。

最开始BFS一次,分层图的层数即为到源点的最短距离。

接下来DFS一次将所有增广路找出且增广。

有一个优化为当前弧优化

即记录当前点有多少条出边是满的,可以有效的降低时间复杂度。

代码实现:

#include<bits/stdc++.h>
#define register signed
using namespace std;
const int MAXN=2e5+5;
const int INF=0x7f7f7f7f;
struct node
{
    int to,nxt,flow;
}e[MAXN];
int head[MAXN],cnt=1;
inline void add(int x,int y,int f)
{
    e[++cnt].to=y;
    e[cnt].flow=f;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}
int n,m,s,t;
int dis[MAXN],cur[MAXN];//dis是分层图层数,cur是当前弧优化
inline bool bfs()//bfs判断是否有增广路以及建立分层图
{
    queue<int>q;
    memset(dis,-1,sizeof(dis));
    dis[s]=0;
    cur[s]=head[s];
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(register int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to,f=e[i].flow;
            if(dis[y]==-1&&f)//层数为-1代表没有走过
            {
                dis[y]=dis[x]+1;
                cur[y]=head[y];//初始化cur[y]为y连出的第一条边
                if(y==t)return true;
                q.push(y);
            }
        }
    }
    return false;
}
inline int dfs(int x,int lim)//dfs找增广路,x是当前到的节点,lim是当前点的流量限制
{
    if(x==t)return lim;
    int f=0;//当前点流出的流量
    for(register int i=cur[x];i&&f<lim;i=e[i].nxt)//从第一条没有满的边出发,且保证流出的流量不超过限制
    {
        int y=e[i].to;
        cur[x]=i;//如果到了第i条边,那么前面的所有边一定都走完了
        if(dis[y]==dis[x]+1&&e[i].flow)//需要保证分层图的限制才能继续
        {
            int t=dfs(y,min(e[i].flow,lim-f));//前往的点的限制即为这条边的容量和剩余流量的中较小的一个
            if(!t)dis[y]=-1;//返回流量为0,说明不能到汇点,标记以后都不走
            e[i].flow-=t;
            e[i^1].flow+=t;
            f+=t;
        }
    }
    return f;
}
inline int dinic()
{
    int ans=0,f;
    while(bfs())while(f=dfs(s,INF))ans+=f;//只要当前残留网络中有增广路(bfs),就找出并累加(dfs)
    return ans;
}
int main()
{
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(register int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        add(y,x,0);
    }
    printf("%d",dinic());
    return 0;
}

时间复杂度较优,为 \(O(n^2m)\)

同样跑不满\(10^4\)\(10^5\) 可跑。

3.最大流的分析方法

对于可行解中的任何一个,可以转化为一个可行流,反之亦可,所以最大可行解等于最大流,求最大流即可。

4.上下界可行流

1.无源汇上下界可行流

现在每条边都有一个流量上界和下界。

求一个满足流量限制的可行流。

依旧考虑流量守恒和容量限制。

设原网络为 \(G\) ,可行流为 \(f\)

现在有一个新的流网络为 \(G'\) 以及可行流 \(f'\) 和正常的流网络类似,而且是由原来的变过来的。

我们需要满足现在的流网络有源点和汇点,且没有下界。

对于 \(G\) 中的一条边 \((x,y)\) ,流量上界为 \(c_u(x,y)\) ,下界为 \(c_l(x,y)\) ,流量为 \(f(x,y)\)

满足 \(c_l(x,y)\leqslant f(x,y)\leqslant c_u(x,y)\)

所以 \(0\leqslant f(x,y)-c_l(x,y)\leqslant c_u(x,y)-c_l(x,y)\)

\(c_u(x,y)-c_l(x,y)\) 当做新网络的流量即可。

1.流量守恒

现在设 \(c_{x出}=\sum\limits^{}_{y\in V}c_l(x,y)\)\(c_{x入}=\sum\limits^{}_{y\in V}c_l(y,x)\)

因为需要满足流量守恒,所以若 \(c_{x入}\gt c_{x出}\) ,则连边 \((S,x,c_{x入}-c_{x出})\) 否则一样。

2.容量限制

满足 \(c_l(x,y)\leqslant f(x,y)\leqslant c_u(x,y)\)

3.代码实现

如下:

#include<bits/stdc++.h>
#define s 0
#define t n+1
using namespace std;
const int MAXN=1e6+5;
const int INF=0x7f7f7f7f;
struct node
{
    int to,nxt,flow;
}e[MAXN];
int head[MAXN],cnt;
int l[MAXN],A[MAXN];
inline void add(int x,int y,int fa,int fb)
{
    e[cnt].to=y;
    e[cnt].flow=fb-fa;
    l[cnt]=fa;
    e[cnt].nxt=head[x];
    head[x]=cnt++;
    e[cnt].to=x;
    e[cnt].flow=0;
    e[cnt].nxt=head[y];
    head[y]=cnt++;
}
int n,m;
int dis[MAXN],cur[MAXN];
inline bool bfs()
{
    queue<int>q;
    memset(dis,-1,sizeof dis);
    dis[s]=0;
    cur[s]=head[s];
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(register int i=head[x];~i;i=e[i].nxt)
        {
            int y=e[i].to,f=e[i].flow;
            if(dis[y]==-1&&f)
            {
                dis[y]=dis[x]+1;
                cur[y]=head[y];
                if(y==t)return true;
                q.push(y);
            }
        }
    }
    return false;
}
inline int dfs(int x,int lim)
{
    if(x==t)return lim;
    int f=0;
    for(register int i=cur[x];~i&&f<lim;i=e[i].nxt)
    {
        int y=e[i].to;
        cur[x]=i;
        if(dis[y]==dis[x]+1&&e[i].flow)
        {
            int w=dfs(y,min(e[i].flow,lim-f));
            if(!w)dis[y]=-1;
            e[i].flow-=w;
            e[i^1].flow+=w;
            f+=w;
        }
    }
    return f;
}
inline int dinic()
{
    int ans=0,f;
    while(bfs())while(f=dfs(s,INF))ans+=f;
    return ans;
}
int main()
{
    memset(head,-1,sizeof head);
    scanf("%d%d",&n,&m);
    for(register int i=0;i<m;i++)
    {
        int x,y,a,b;
        scanf("%d%d%d%d",&x,&y,&a,&b);
        add(x,y,a,b);
        A[x]-=a;
        A[y]+=a;
    }
    int sum=0;
    for(register int i=1;i<=n;i++)
    {
        if(A[i]>0)
        {
            add(s,i,0,A[i]);
            sum+=A[i];
        }
        else if(A[i]<0)add(i,t,0,-A[i]);
    }
    if(dinic()!=sum)puts("NO");
    else
    {
        puts("YES");
        for(register int i=0;i<m*2;i+=2)
            printf("%d\n",e[i^1].flow+l[i]);
    }
    return 0;
}

5.费用流

现在每条边有一个费用 \(c_i\) 表示每个流量需要这么多费用。

需要在最小费用的前提下获得最大流量。

这里用的是最简单的 EK+SPFA

首先因为需要的是最小费用,所以需要求一个到汇点的费用最小的增广路。

这时候就需要一个最短路算法。

因为 dij 不能处理负边权,所以直接用 SPFA 跑出最小费用。

找没找到一条增广路就看汇点的流量是否不为零即可。

接下来再用 EK 减去增广路上的流量。

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
const int INF=0x7f7f7f7f;
struct node
{
	int to,nxt,flow,cost;
}e[MAXN];
int head[MAXN],cnt=1;
int n,m,s,t;
inline void adde(int x,int y,int f,int c)
{
	e[++cnt].to=y;
	e[cnt].flow=f;
	e[cnt].cost=c;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}
inline void add(int x,int y,int f,int c)
{
	adde(x,y,f,c);
	adde(y,x,0,-c);
}
int dis[MAXN],incf[MAXN],pre[MAXN];
//dis表示到某个点的最小费用,incf表示到某个点的最大流量,pre表示EK中前一条边 
bitset<MAXN>vis;
inline bool spfa()
{
	queue<int>q;
	memset(dis,0x7f,sizeof dis);//最短路,极大值 
	memset(incf,0,sizeof incf);//最开始没有流量 
	q.push(s);
	dis[s]=0;
	incf[s]=INF;//源点流量肯定为INF 
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		vis[x]=0;
		for(register int i=head[x];i;i=e[i].nxt)
		{
			int y=e[i].to,f=e[i].flow,c=e[i].cost;
			if(f&&dis[y]>dis[x]+c)
			{
				dis[y]=dis[x]+c;
				incf[y]=min(incf[x],f);
				pre[y]=i;
				if(!vis[y])
				{
					q.push(y);
					vis[y]=1;
				}
			}
		}
	}
	return incf[t];
}
int flow,cost;
inline void EK()
{
	while(spfa())
	{
		flow+=incf[t];//累加流量 
		cost+=incf[t]*dis[t];//费用很容易推 
		/*
		       pre[y]
		     -------->
		    x    i    y
		     <--------
		     pre[y]^1
		*/ 
		for(register int i=t;i!=s;i=e[pre[i]^1].to)//前往上一个点 
		{
			e[pre[i]].flow-=incf[t];//pre[i]为正向边 
			e[pre[i]^1].flow+=incf[t];//为反向边 
		}
	}
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(register int i=1;i<=m;i++)
	{
		int x,y,f,c;
		scanf("%d%d%d%d",&x,&y,&f,&c);
		add(x,y,f,c);
	}
	EK();
	printf("%d %d",flow,cost);
	return 0;
}
posted @ 2022-02-20 12:46  yzh_Error404  阅读(76)  评论(0)    收藏  举报