网络流

网络流

杂项

网络流初探,为学习,故写此篇,在补

​ ---liuqichen121

网络流的概念和定义多而杂,重要而又容易混淆

一些基本的符号:

  • \(G\) :流网络
  • \(G_f\) :残留网络
  • \(V\) :点集
  • \(E\) :边集
  • \(f\) :流
  • \(c\) :流量限制

定义(概念)

流网络

  • 流网络是一个有向图,有两个特殊的点(源点 \(s\) 、汇点 \(t\)

​ 源点可以流出无穷多的流量,汇点可以容纳无穷多的容量

  • 包含一个点集和一个边集

可行流

  • 对于每个流网络,可以指定可行流(可行的整张图的流)

    可行流是相对于其相应的流网络来说的,它的成立需要满足两个条件:

    第一个条件:流量限制 每个边的流量小于流量限制 \(0\le f(u,v)\le c(u,v)\)

    第二个条件:流量守恒 每个节点流入流量等于流出流量

    在《算导(第三版)》中还有第三个性质:反对称性:对于所有 \(u,v\in V\) ,要求 \(f(u,v)=-f(v,u)\) ,其中集合 \(V\) 表示全图的点集

  • \(f(u,v)\) 称为从顶点 \(u\) 到顶点 \(v\) 的流,它可以为正、为零也可以为负。流 \(f\) 的值定义为:\(|f|=\sum_{v\in V}f(s,v)\) ,即从源点出发,到其他所有点流量的总和。

    这里用 “ \(|*|\) ” 表示流的值,而不是绝对值或其他。

  • \((u,v)\)\((v,u)\) 都不在 \(E\) 中,则 \(u\)\(v\) 之间不可能有网络流,即 \(f(u,v)=f(v,u)=0\)

  • 根据反对称性,可以将流量守恒重写为: \(\sum_{u\in V}f(u,v)=0\) ,亦即进入一个顶点的总流量为 \(0\)

  • 进入一个点 \(u\) 的总的正网络流定义为 \(|f|=\sum_{v\in V,f(u,v)>0}f(s,v)\)

    离开某顶点的正网络流是对称的进行定义的。定义某个顶点处的总的净流量为 离开此点的总的正流量减去该点的正流量

    流量守恒可形象化的称为“流进等于流出”

  • “最大流” 指 “最大可行流

残留网络

  • 也属于一个网络流

  • 针对可行流(一一对应),记为 \(G_f\)

  • 包含原图的点集、 \(E\) (原图)中的边集和 \(E\) 中的所有反向边

  • 每条反向边的流量限制 \(c'(u,v)=c(u,v)-f(u,v)\)

  • 反向边的流量:可以退回的流量 \(f(v,u)\le c(v,u)\)

  • 也存在相应的可行流 \(f'\) ,原图 \(E\) 的可行流为 \(|f+f'|=|f|+|f'|\) ,证明利用两个条件:

    这里再对符号进行一次区分:

    \(f\) :原图的可行流

    \(f'\) :残留网络的可行流

    \(c\) :边的流量限制

    1. 流量限制

      由于 \(0\le f'(u,v)\le c(u,v)-f(u,v)\)

      所以 \(0 \le f'(u,v)+f(u,v)\le c(u,c)\)

      \(f'(u,v)+f(u,v)\) 即每个边的流量限制

      所以满足流量限制

    2. 流量守恒

      由于 \(f\)\(f'\) 都守恒

      所以他们相对也是守恒的

  • 如果残留网络没有可行流,则原网络一定是最大可行流

  • 增广路径:在残留网络中,沿着容量大于 \(0\) 边走,可以从源点到汇点的路径,即相应的原网络的增广路径

割(切割)

  • 把流网络的点集 \(V\) 分为不重不漏的两个子集 \(S\)\(T\) ,使得 \(S\cap T=\phi\)\(S\cup T=V\) 满足 \(s\in S\)\(t\in T\) ,分割的结果称为图的割

    割的图相对于原图不一定完整,即不一定包含所有的边

  • 相当于将图按点分为两部分

  • 割的容量:所有 \(S\) 指向 \(T\) 的边的容量(即流量限制)之和

    最小割指割的容量的最小值,最大流指流量流的最大值

  • 割的流量:流出流量总和减去流回流量总和

    \(\sum_{u\in S}\sum_{v\in T}f(u,v)-\sum_{u\in T}\sum_{v\in S}f(u,v)\)

  • 对于任意一个割,割的流量不大于割的容量 \(f(S,T)\le c(S,T)\)

  • 对于任意一个割的任意可行流 \(f(S,T)=|f|\)

  • 一些公式、性质

    • \(f(X,Y)=-f(Y,X)\)

    • \(f(X,X)=0\)

    • \(f(Z,X\cup Y)=f(Z,X)+f(Z,Y)[X\cap Y=\phi时]\)

    • \(f(X\cup Y,Z)=f(X,Z)+f(Y,Z)\)

  • 最大流的流量 \(\le\) 最小割的流量

最大流最小割定理

  • 对于一个 \(G\) 来说

    1. \(f\) 是最大流

    2. 残留网络 \(G_f\) 中不存在增广路径

    3. 存在割 \([S,T]\) 使 \(|f|=c(S,T)\)

      以上三个条件等价,即可以相互印证

算法

EK算法

EK算法简介

任务:实现对网络流中最大流的寻找

核心:利用 \(BFS\) 不断在残留网络中寻找增广路径

方法:

  1. 找增广路径(在残留网络中找可以从源点到汇点的路径)
  2. 更新残留网络(正向边减去增广路径的流量,反向边加上增广路径长度,即将残留网络流量转移到原网络流上)
  3. 移至 (1) 反复执行,直至找不到增广路径

时间复杂度: \(O(nm^2)\)

在图论中 \(n\) 一般指节点数, \(m\) 一般指边数

EK算法求最大流模板

#include<algorithm>
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<string>
#include<vector>
#include<math.h>
#include<stack>
#include<queue>
#include<set>
#include<map>
//不要在意
using namespace std;

//N:点的上限 M:边的上限 INF:可行流数值大小的上限
const int N=1e3+10,M=2e4+10,INF=1e9;

int n,m,S,T;
//n:点数 m:边数 S:原点编号 T:会点编号

//利用链式前向星存图
struct no{
    int v,to,c;
    //v:指向节点 to:上一条边 c:边的容量
}node[M];
int head[N],idx=1;

//添边操作
void add(int u,int v,int c);

//q:bfs中的队列 d:每个节点的流量 pre:每个边的前驱节点
int q[N],d[N],pre[N];

//在bfs时标记节点是否遍历过
bool st[N];

//bfs查找残留网络中是否还有可行的增广路径
bool bfs();

//利用EK算法找最大流
int EKalgorithm();

int main(){

    scanf("%d%d%d%d",&n,&m,&S,&T);
    //输入点数、边数、源点编号、汇点编号

    //输入、存图
    for(int i=1;i<=m;i++){
        int u,v,c;
        scanf("%d%d%d",&u,&v,&c);
        add(u,v,c);
    }

    //输出EK算法得到的最大流
    printf("%d",EKalgorithm());

    return 0;
}
void add(int u,int v,int c){

/*
    添边时要注意:
        不仅要添其原图边
        还有添加它的反向边
    为了方便查找正/反向边:
        可以将正边编号为2、4……
        将反边编号为3、5……
        使正边的idx(编号)^1==反边的idx(编号)
*/
    //正常的添边操作
    idx++;
    node[idx].v=v;
    node[idx].to=head[u];
    head[u]=idx;
    //由于初始时正向边的流为0,所以其容量还有c
    node[idx].c=c;

    //添反向边
    idx++;
    node[idx].v=u;
    node[idx].to=head[v];
    head[v]=idx;
    //由于初始时反向边可看作流达到上限
    //所以其容量为0
    node[idx].c=0;

    return;
}
bool bfs(){
    //初始化
    //清空标记数组
    for(int i=1;i<=n;i++)
    st[i]=0;
    //清空队列、只留一个位置
    int h=0,t=0;
    //将源点放入队列
    q[0]=S;
    //标记源点
    st[S]=1;
    //将源点的流设为最大
    d[S]=INF;

    //进行bfs搜索
    while(h<=t){
        int u=q[h++];
        //链式前向星取边
        for(int i=head[u];i;i=node[i].to){
            int v=node[i].v;
            //如果下一个节点尚未被标记并且其尚有容量(即可以流)
            if(!st[v]&&node[i].c){
                //进行标记
                st[v]=1;
                //将流量转移
                d[v]=min(d[u],node[i].c);
                //记录前驱节点(为记录路线)
                pre[v]=i;
                //如果可以到达汇点即存在增广路径
                if(v==T)
                return 1;
                //存入队列
                q[++t]=v;
            }
        }
    }
    return 0;
}
int EKalgorithm(){

    //r:反向图中当前bfs到的增广路径
    int r=0;

    /*
        EK持续的条件为:
            一直有增广路径
        所以可以将bfs返回bool值作为EK持续的条件
    */
    //进行bfs判断是否含有增广路径
    while(bfs()){
        //记录当前可行流的值(加不动时就成最大流了)
        r+=d[T];
        //从汇点开始寻找前驱节点直至源点
        for(int i=T;i!=S;i=node[pre[i]^1].v){
            //将正向边容量减小,反向边反之
            node[pre[i]].c-=d[T];
            node[pre[i]^1].c+=d[T];
        }
    }
    return r;
}

dinic算法

dinic算法简介

任务:求出网络流的最大流

核心:利用最大流最小割定理找增广路径确定最大流(基本同 \(EK\) 算法相同)

优化:当前弧优化

当前弧优化:

​ 由于链式前向星的存边

​ 所以对边的访问顺序固定

​ 因此一定是在前面的边先达到容量

​ 因此可以记录下来并跳过,达到优化

方式:

  1. 引用分层图
  2. 严格要求只能从前一层走到后一层(防止环的影响)
  3. 通过爆搜增广所有增广路径,进行优化,统一增广

时间复杂度: \(O(n^2m)\)

dinic算法求最大流模板

#include<algorithm>
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<string>
#include<vector>
#include<math.h>
#include<stack>
#include<queue>
#include<set>
#include<map>
using namespace std;

//N:点的上限 M:边的上限 INF:可行流数值大小的上限
const int N=1e3+10,M=2e4+10,INF=1e9;

//n:点数 m:边数 S:原点编号 T:汇点编号
int n,m,S,T;

//利用链式前向星存图
struct no{
    int v,to,c;
    //v:指向节点 to:上一条边 c:边的容量
}node[M];
int head[N],idx=1;

void add(int u,int v,int c){

    //正常的添边操作(同EK算法)
    idx++;
    node[idx].v=v;
    node[idx].to=head[u];
    head[u]=idx;
    node[idx].c=c;
    idx++;
    node[idx].v=u;
    node[idx].to=head[v];
    head[v]=idx;
    node[idx].c=0;

    return;
}

int q[N],d[N];

//当前弧优化(跳过已达容量上限的边)
int cur[N];

/*
    当前弧优化:
        由于链式前向星的存边
        所以对边的访问顺序固定
        因此一定是在前面的边先达到容量
        因此可以记录下来并跳过,达到优化
*/

//bfs找增广路径
bool bfs(){

    //初始化
    //将所有点的深度初始为-1
    for(int i=1;i<=n;i++)
    d[i]=-1;
    //初始化队列
    int h=0,t=0;
    q[0]=S,d[S]=0;
    //将S(源点)的当前弧设为第一个边
    cur[S]=head[S];

    while(h<=t){
        int u=q[h++];
        for(int i=head[u];i;i=node[i].to){
            int v=node[i].v;
            if(d[v]==-1&&node[i].c){
                d[v]=d[u]+1;
                cur[v]=head[v];
                if(v==T)
                return 1;
                q[++t]=v;
            }
        }
    }
    return 0;
}

int find(int u,int limit){
    
    if(u==T)
    return limit;
    
    int flow=0;
    //从cur[u]开始(跳过满的边)
    for(int i=cur[u];i&&flow<limit/*防止无效搜索*/;i=node[i].to){
        //当前弧优化
        cur[u]=i;//因为i前面的边已经被用完,所以可以直接更新
        int v=node[i].v;
        //只有在下一层才有必要搜
        if(d[v]==d[u]+1&&node[i].c){
            int t=find(v,min(node[i].c,limit-flow));
            //如果不存在增广路径则删边/点
            if(!t)d[v]=-1;
            //删去增广路径
            node[i].c-=t;
            node[i^1].c+=t;
            flow+=t;
        }
    }
    return flow;
}

int dinic(){
    //r:当前流 flow:增广路径增加的流
    int r=0,flow;
    
    //进行对增广路径的查找,并加入到当前流中
    while(bfs())
    while(flow=find(S,INF))
    /*
        这里可能会报错:
            因为它可能会认为你将判断语句的“==”
            误打成了赋值语句“=”
        如果不想报错的话可以多加一层括号
    */
    r+=flow;

    return r;
}

int main(){
    //输入
    scanf("%d%d%d%d",&n,&m,&S,&T);
    //存储图
    for(int i=1;i<=m;i++){
        int u,v,c;
        scanf("%d%d%d",&u,&v,&c);
        add(u,v,c);
    }
    //进行dinic找最大流
    printf("%d\n",dinic());

    return 0;
}

最大流习题

上下界可行流

无源汇上下界可行流

\(c_c\) :流量下界

\(c_u\) :流量上界

题目

无源汇上下界可行流

题目要求

现在有一个有向图

实现在有向图中完成以下要求:

  • 流量守恒

  • 容量限制

    • 容量下界

    • 容量上界

      即对于每个流 \(c_c(u,v)\le f(u,v)\le c_u(u,v)\)

思路

实现将上下界改为仅一个上界的一般形式

\(c_c(u,v)\le f(u,v)\le c_u(u,v)\) 的各项减去一项 \(c_c(u,v)\) 得到下界的限制为 \(0\)

即 需要满足新的条件

\(0\le f(u,v)-c_c(u,v)\le c_u(u,v)-c_c(u,v)\)

即可满足上下界条件

对于新的条件

则需要建一个新的图 \(G'\) 使

\(0\le f'(u,v)\le c'(u,v)\)

\(f'(u,v)=f(u,v)-c_c(u,v)\)

\(c'(u,v)=c_u(u,v)-c_c(u,v)\)

满足

新图 \(G\) 可以达到最大流,则原图可以存在满足条件的上下界可行流

模板代码

#include<algorithm>
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<string>
#include<vector>
#include<math.h>
#include<stack>
#include<queue>
#include<set>
#include<map>
using namespace std;
const int N=2e2+10,M=(1.02e4+N)*2,INF=1e9;
int n,m,S,T;
struct no{
	int to,v,c,l;
}node[M];
int head[N],idx=1;
int q[N],d[N],cur[N],A[N];
void add(int u,int v,int l,int c){
	idx++;
	node[idx].v=v;
	node[idx].c=c-l;
	node[idx].l=l;
	node[idx].to=head[u];
	head[u]=idx;
	idx++;
	node[idx].v=u;
	node[idx].c=0;
	node[idx].to=head[v];
	head[v]=idx;
}
bool bfs(){
	int h=0,t=0;
	for(int i=0;i<N;i++)
		d[i]=-1;
	q[0]=S,d[S]=0;
	cur[S]=head[S];
	while(h<=t){
		int u=q[h++];
		for(int i=head[u];i;i=node[i].to){
			int v=node[i].v;
			if(d[v]==-1&&node[i].c){
				d[v]=d[u]+1;
				cur[v]=head[v];
				if(v==T)
				return 1;
				q[++t]=v;
			}
		}
	}
	return 0;
}
int find(int u,int limit){
	if(u==T)
	return limit;
	int flow=0;
	for(int i=cur[u];i&&flow<limit;i=node[i].to){
		cur[u]=i;
		int v=node[i].v;
		if(d[v]==d[u]+1&&node[i].c){
			int t=find(v,min(node[i].c,limit-flow));
			if(!t)
			d[v]=-1;
			node[i].c-=t;
			node[i^1].c+=t;
			flow+=t;
		}
	}
	return flow;
}
int dinic(){
	int r=0,flow;
	while(bfs()){
		while(flow=find(S,INF))
		r+=flow;
	}
	return r;
}
int main(){
	scanf("%d%d",&n,&m);
	S=0,T=n+1;
	for(int i=1;i<=m;i++){
		int a,b,c,d;
		scanf("%d%d%d%d",&a,&b,&c,&d);
		add(a,b,c,d);
		A[a]-=c;
		A[b]+=c;
	}
	int tot=0;
	for(int i=1;i<=n;i++){
		if(A[i]>0)
			add(S,i,0,A[i]),tot+=A[i];
		else if(A[i]<0)
			add(i,T,0,-A[i]);
	}
	if(dinic()!=tot){
		printf("NO\n");
	}else{
		printf("YES\n");
		for(int i=2;i<=m*2;i+=2){
			printf("%d\n",node[i^1].c+node[i].l);
		}
	}
	return 0;
}

有源汇有上下界最大流

题目

有源汇有上下界最大流

题目要求

此题相对于 无源汇上下界可行流 一题添加了一些对流的限制

除了对每条边做到满足 容量守恒 与 容量限制 以外还添加了两个条件

  • 给出了指定的源点和汇点
  • 满足最大可行流

思路

对于给定源点和汇点的有向图,可以在源点和汇点之间建立一条由汇点指向源点容量上限不限的边,即新建一个边使 \(c(T,S)=INF\) ,将有源汇问题转化为无源汇问题

模板代码

#include<algorithm>
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<string>
#include<vector>
#include<math.h>
#include<stack>
#include<queue>
#include<set>
#include<map>
using namespace std;
const int N=2e2+10,M=(N+1e4+10)*2,INF=1e8;
int n,m,S,T;
struct no{
	int v,to,f;
}node[M];
int head[N],idx=1;
int q[N],d[N],cur[N],A[N];
void add(int u,int v,int f){
	idx++;
	node[idx].f=f;
	node[idx].v=v;
	node[idx].to=head[u];
	head[u]=idx;
	idx++;
	node[idx].f=0;
	node[idx].v=u;
	node[idx].to=head[v];
	head[v]=idx;
	return;
}
bool bfs(){
	for(int i=0;i<=n+1;i++){
		d[i]=-1;
	}
	int h=0,t=0;
	q[0]=S,d[S]=0;
	cur[S]=head[S];
	while(h<=t){
		int u=q[h++];
		for(int i=head[u];i;i=node[i].to){
			int v=node[i].v;
			if(d[v]==-1&&node[i].f){
				d[v]=d[u]+1;
				cur[v]=head[v];
				if(v==T)
				return 1;
				q[++t]=v;
			}
		}
	}
	return 0;
}
int find(int u,int limit){
	if(u==T)
	return limit;
	int flow=0;
	for(int i=cur[u];i&&flow<limit;i=node[i].to){
		cur[u]=i;
		int v=node[i].v;
		if(d[v]==d[u]+1&&node[i].f){
			int t=find(v,min(node[i].f,limit-flow));
			if(!t)
			d[v]=-1;
			node[i].f-=t;
			node[i^1].f+=t;
			flow+=t;
		}
	}
	return flow;
}
int dinic(){
	int r=0,flow;
	while(bfs()){
		while(flow=find(S,INF))
		r+=flow;
	}
	return r;
}
int main(){
	int s,t;
	scanf("%d%d%d%d",&n,&m,&s,&t);
	S=0,T=n+1;
	for(int i=1;i<=m;i++){
		int u,v,l,p;
		scanf("%d%d%d%d",&u,&v,&l,&p);
		add(u,v,p-l);
		A[u]-=l,A[v]+=l;
	}
	int tot=0;
	for(int i=1;i<=n;i++){
		if(A[i]>0)
		add(S,i,A[i]),tot+=A[i];
		else if(A[i]<0)
		add(i,T,-A[i]);
	}
	add(t,s,INF);
	if(dinic()<tot){
		printf("please go home to sleep\n");
	}else{
		int res=node[idx].f;
		S=s,T=t;
		node[idx].f=node[idx-1].f=0;
		printf("%d\n",res+dinic());
	}
	return 0;
} 

题目

posted @ 2024-01-25 14:53  liuqichen121  阅读(40)  评论(0)    收藏  举报