网络流初步
网络流初步
文章目录
- 网络流初步
- 概念介绍
- 最大流
- 费用流
概念介绍
网络流不同之处在于它的本质图论,但是把图论的某些概念换了一个说法而已,初步只要了解网络流的各个概念就可以明白的很快。
下述概念是本人自己定义的,对于网络流的题目做的还不多,后续会更正。
首先需要理解:网络流其实是在一个有向图中,从某个原点开始放水,水沿着管道最后流向汇点的过程,基于此就可以很好的理解一些概念和定理,而不需要十分严谨的证明(因为证明意义不大
流网络: 对于一个 G(V,E)G(V,E)G(V,E) 的单向图,我们称为流网络
容量C: 点和点之间的边权成为容量
流量F: 通俗点说,点与点之间流过的水流量,和物理定义流量一致。一个很显然的性质:F≤CF\le CF≤C 即流量不能超过容量
原点和汇点: 水流出的点,和水流入的点。
可行流: 全称可以流进去的流量,F(u,v)F(u,v)F(u,v) 表示从 u 点流到 v 点的流量
流网络的可行流:即从原点或者汇点流入或者流出的总流量 ∣F∣|F|∣F∣
最大流: 全称最大可行流,比较抽象,实际意义就是:从原点可以流出的最大流量,或者是汇集到汇点的最大流量。
残留网络: 对于一个每条边都确定流量F的流网络,如果存在某条边 C−F>0C-F>0C−F>0 ,即如果可以的话,还可以从 u→vu\to vu→v 流动 C−FC-FC−F 的水,那么我们将这些差值重新构造一张图,形成的网络被称为残留网络,十分形象,同时还会建立反向边,不过权值变成 −F-F−F ,可以理解为水反向流动。此时构成完整的残存网络。
增广路: 在残存网络中,存在一条路径使得从S到T,该条路径流过的水>0,也就是路径最小值大于零,则称为该条路径成为增广路。
割:对于原点S 和 汇点T,我们通过划分将点集分成两部分,满足 S点集中点可以从S到达,T点集中点可以到达T,但是T和S不能在一个集合,此时我们成为是流网络中的一个割。
割的容量: 即连接点集 S 和点集 T 的边权的容量
割的流量: 同上,边权的流量
最小割: 全称最小割的容量。
结论
-
如果残留网络不存在增广路,此时流网络的可行流最大,即最大流
证明: 主观十分好理解:如果不存在一种路径使得水从原点流到汇点,那么此时说明不能再流动,那么此时就是最大流量
证明充要:也很显然,既然是最大流量,那么就不可能存在让自己流量在增加的路
-
如果残留网络不存在增广路,那么一定存在一种割的两个点集 S,TS,TS,T,满足 ∣F∣=∑u∈S,v∈Tc(u,v)|F|=\sum_{u\in S,v\in T} c(u,v)∣F∣=∑u∈S,v∈Tc(u,v)
证明:现在画图更好理解这句话的意思:
公式含义就是粉色框的边权的容量要等于流网络的可行流。 y总的证明:
我们这样构造 S点集:在残留网络GfG_fGf 中所有从S点出发边权容量不是 0 的点,剩余点归算于T点,那么对于这割的容量就是0,可以发现,这样的集合是可以构造出来的,因为有这个GfG_fGf 不存在增广路的条件,那么所有的路径至少存在一个边权容量为0。
那么因为每个割的残留网络容量为0,那么说明每条边都是某条流过的路径的最小值(或者是某些最小值的和,因为可能会存在距离S更近的某条边残余容量为0,这些流量之后恰好分开流向后面的最小值们,本质上是还是所有流过的路径最小值的贡献总和,可以理解为是所有的最小值边成为了割),也就是该条路径的流量,那么将这些流量加起来就是总流量。因为从S点流出的流量必须经过割中的所有边权才能到达T,又因为每个边的容量等于流量,所以满足 ∣F∣=∑C(u,v)|F|=\sum C(u,v)∣F∣=∑C(u,v)
-
如果存在两个点集 S,TS,TS,T,满足 ∣F∣=∑u∈S,v∈Tc(u,v)|F|=\sum_{u\in S,v\in T} c(u,v)∣F∣=∑u∈S,v∈Tc(u,v),那么∣F∣|F|∣F∣ 一定是最大流
证明:很显然,因为割的每条边的流量完全等于容量(由2可知(因为不存在增广路)),试想,我们将连接两个点集的之间割的容量全部得到了(S,T 之间可传递的最大容量),那么此时的流量和就是最大流。
-
最大流=最小割
由上面的三个证明便可以得到,最大流 →\to→ 不存在增广路 →\to→ 最小割 →\to→ 最大流
这是最重要的证明,便于以后的建边和理解。
最大流
算法实现:
EK算法
算法思想:在图中不断找增广路,然后对边更改删掉增光路,对答案造成贡献,最后不存在增广路就是答案。时间复杂度:O(nm2)O(nm^2)O(nm2),时间复杂度不会证明,不过可以满足 n<10000 以下的级别
模板:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
int p;
int n,m,S,T;
int head[1010];
int cnt;
struct node
{int u,v,nxt,w;
}e[B<<1];
void modify(int u,int v,int w)
{e[cnt].nxt=head[u];e[cnt].v=v;e[cnt].w=w;head[u]=cnt;cnt++;
}
void add(int u,int v,int w)
{modify(u,v,w);modify(v,u,0);
}
int d[1010];
int pre[1010];
int vis[1010];
int inf=0x3f3f3f3f;
bool bfs()
{queue<int>q;memset(vis,0,sizeof(vis));q.push(S); vis[S]=1; d[S]=inf; while (!q.empty()){int u=q.front();q.pop();for (int i=head[u];i!=-1;i=e[i].nxt){int v=e[i].v;if (vis[v] || e[i].w==0) continue;d[v]=min(d[u],e[i].w);vis[v]=1;pre[v]=i;if (T==v) return 1;q.push(v);}}return 0;
}
int EK()
{int r=0;while (bfs()){r+=d[T];for (int i=T;i!=S;i=e[pre[i]^1].v){e[pre[i]].w-=d[T];e[pre[i]^1].w+=d[T];}}return r;
}
void work()
{cin>>n>>m>>S>>T;memset(head,-1,sizeof(head));for (int i=1;i<=m;i++){int u=read(),v=read(),c=read();add(u,v,c);}cout<<EK();
}
signed main()
{p=1;while (p--) work();return 0;
}
网络流细节
双向建边的时候需要注意的地方
- cnt从 0 开始,不是1
- for 循环里,边界 i!=-1 而不是 i!=0
费用流
费用流被称为:最小费用最大流
每个流量上又添加了一个单位流量花费。要求在最大流的情况下花费最小。
写法:把bfs找增广路,改写成SPFA找增广路就可以
证明:
最终可以从s点到达T做出贡献的路径是一定的,每条路径之间可能存在平替的现象,Bfs只是从中随便选择了几条,但我们可以将其平替,如下图
这些路径中,存在多种方案满足条件,要想花费最小,直接贪心就可。以前之所没有想明白,就可这张图一样,我想的是如果先把 6 的路径找出来,最后就只剩下 2 的路径才能成为8
不过我忘记了,还可以从 4 中走2,并不一定非要走4,所以这就不需要担心当前选择影响后续可选择对象的问题了,因为当前的选择不影响后续任何选择,不会导致选择对象数量发生变化,故可以直接贪心
模板
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
int p;
int n,m,S,T;
int head[B];
int cnt;
struct node
{int u,v,nxt,c,w;
}e[B<<1];
void modify(int u,int v,int c,int w)
{e[cnt].nxt=head[u];e[cnt].v=v;e[cnt].c=c;e[cnt].w=w;head[u]=cnt;cnt++;
}
void add(int u,int v,int c,int w)
{modify(u,v,c,w);modify(v,u,0,-w);
}
int d[5010];
int pre[5010];
int vis[5010];
int inf=0x3f3f3f3f;
int dis[5010];
bool bfs()
{queue<int>q;for (int i=1;i<=n;i++){vis[i]=0;dis[i]=inf;}q.push(S); vis[S]=1; d[S]=inf; dis[S]=0;while (!q.empty()){int u=q.front();q.pop();vis[u]=0;for (int i=head[u];i!=-1;i=e[i].nxt){int v=e[i].v;if (e[i].c==0) continue;if (dis[v]>dis[u]+e[i].w){dis[v]=dis[u]+e[i].w;pre[v]=i;d[v]=min(d[u],e[i].c);if (!vis[v]){vis[v]=1;q.push(v);}}}}return dis[T]!=inf;
}
void EK()
{int r=0;int sum=0;while (bfs()){r+=d[T];sum+=d[T]*dis[T];for (int i=T;i!=S;i=e[pre[i]^1].v){e[pre[i]].c-=d[T];e[pre[i]^1].c+=d[T];}}printf("%lld %lld", r,sum);
// cout<<r<<" "<<sum<<"\n";
}
void work()
{cin>>n>>m>>S>>T;for (int i=1;i<=n;i++) head[i]=-1;for (int i=1;i<=m;i++){int u=read(),v=read(),c=read(),w=read();add(u,v,c,w);}EK();
}
signed main()
{p=1;while (p--) work();return 0;
}
r,sum);
// cout<<r<<" “<<sum<<”\n";
}
void work()
{
cin>>n>>m>>S>>T;
for (int i=1;i<=n;i++) head[i]=-1;
for (int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read(),w=read();
add(u,v,c,w);
}
EK();
}
signed main()
{
p=1;
while (p–) work();
return 0;
}