网络流
网络流
杂项
网络流初探,为学习,故写此篇,在补
---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\) :边的流量限制
-
流量限制
由于 \(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)\) 即每个边的流量限制
所以满足流量限制
-
流量守恒
由于 \(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\) 来说
-
\(f\) 是最大流
-
残留网络 \(G_f\) 中不存在增广路径
-
存在割 \([S,T]\) 使 \(|f|=c(S,T)\)
以上三个条件等价,即可以相互印证
-
算法
EK算法
EK算法简介
任务:实现对网络流中最大流的寻找
核心:利用 \(BFS\) 不断在残留网络中寻找增广路径
方法:
- 找增广路径(在残留网络中找可以从源点到汇点的路径)
- 更新残留网络(正向边减去增广路径的流量,反向边加上增广路径长度,即将残留网络流量转移到原网络流上)
- 移至 (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\) 算法相同)
优化:当前弧优化
当前弧优化:
由于链式前向星的存边
所以对边的访问顺序固定
因此一定是在前面的边先达到容量
因此可以记录下来并跳过,达到优化
方式:
- 引用分层图
- 严格要求只能从前一层走到后一层(防止环的影响)
- 通过爆搜增广所有增广路径,进行优化,统一增广
时间复杂度: \(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;
}
最大流习题
-
luogu
-
如果过不了可以仔细阅读一下数据范围
-
上下界可行流
无源汇上下界可行流
\(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;
}
题目
- LOJ

浙公网安备 33010602011771号