网络流
前言:关于网络流有个生动的比喻,想象一个自来水厂向各处供水,自来水厂有无限多的水,但每条管子单位时间内允许的最大流量有限,现在钦定一个出水口为汇点,现在要做的就是在满足每一条管子不爆的情况下,最大化汇点流出的水量。
定义
1.网络
对于有向图 \(G = (V,E)\) ,其中每条边 \((u,v) \in E\) 都有权值 \(w(u,v)\) ,称之为容量,图中有两个特殊的点 \(s\) , \(t\) \((s\not= 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) 若 \((v,u) \in E\) ,则 \(f(u,v)=-f(v,u)\) ,即一条边的流量与其相反边的流量互为相反
数(注意不是反向边,反向边与相反边的区别是反向边 \((v,u) \notin E\) )。
(3) \(\forall x \in E -\) {\(s,t\)} , \(\sum_{(u,x)\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\) 的路
径,称为一条增广路。
最大流
FF算法
最暴力的最大流算法,每次直接dfs找增广路,找不到了就完成。
ee,因为跑得太慢了当时学了接着就忘了。
EK算法
如上所述,EK就是bfs版的FF算法。
但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:
int bfs(){
memset(let,0,sizeof(let));
queue<int>q;
q.push(be);
flow[be]=1e9;
while(!q.empty()){
int now=q.front();q.pop();
if(now==en) break;
for(int i=head[now];i;i=a[i].next){
int v=a[i].v,w=a[i].w;
if(w>0&&!let[v]){
let[v]=i;
flow[v]=min(flow[now],w);
q.push(v);
}
}
}
return let[en];
}
int EK(){
int ans=0;
while(bfs()){
ans+=flow[en];
for(int i=en;i!=be;i=a[let[i]^1].v){
a[let[i]].w-=flow[en];
a[let[i]^1].w+=flow[en];
}
}
return ans;
}
signed main()
{
n=read();m=read();be=read();en=read();
for(int i=1;i<=m;i++){
x=read();y=read();z=read();
add(x,y,z);add(y,x,0);
}
cout<<EK();
return 0;
}
Dinic算法
然而,最常用的网络流算法是Dinic算法。作为FF/EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是 。
所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为 不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m,be,en,cnt=1,head[N],cur[N],dep[N];//找相反边的一个小技巧
struct node{
int u,v,w,nxt;
}a[N];
void add(int u,int v,int w){
a[++cnt]=(node){u,v,w,head[u]};
head[u]=cnt;
}
inline int read(){char ch=getchar();int f=0,x=0;
while(ch<'0'||ch>'9'){if(ch=='-') f=1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return f?-x:x;
}
int bfs(){//分层
memset(dep,0,sizeof(dep));
queue<int>q;
q.push(be);
dep[be]=1;
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=head[now];i;i=a[i].nxt){
if(!dep[a[i].v]&&a[i].w){
dep[a[i].v]=dep[now]+1;
q.push(a[i].v);
if(a[i].v==en) return 1;//有增广路且分层完毕
}
}
}
return 0;//没搜到汇点,没增广路了
}
int dfs(int k,int flw){//寻找
if(k==en) return flw;//当前能提供的最大流量
int sum=0;
for(int i=cur[k];i;i=a[i].nxt){//多路增广
cur[k]=i;//当前弧优化,本次dfs的下次访问直接从当前弧开始
int v=a[i].v,&w=a[i].w;
if(dep[v]==dep[k]+1&&w){//分层限制层数
int f=dfs(v,min(w,flw));
sum+=f;flw-=f;
a[i^1].w+=f;w-=f;
if(flw==0) break;//没有流量了,停止遍历
}
}
if(sum==0) dep[k]=0;
return sum;
}
int Dinic(){
int ans=0;
while(bfs()){
memcpy(cur,head,sizeof(head));//head -> cur
ans+=dfs(be,1e9);
}
return ans;
}
signed main()
{
n=read();m=read();be=read();en=read();
for(int i=1;i<=m;i++){
int x=read(),y=read(),z=read();
add(x,y,z);
add(y,x,0);
}
cout<<Dinic();
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\) 割,所以最大流 \(=\) 最小割。
费用流
我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用 ,但是为了惠民,自来水厂想找到一种方法,使得流最大的同时费用最小,这就是最小费用最大流。
回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,可以跑最短路,有负权就用spfa,可以判负环。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m,be,en,cnt=1,ans1,ans2,head[N],vis[N],dis[N],pre[N],flow[N];
struct node{
int u,v,w,c,nxt;
}a[N];
void add(int u,int v,int w,int c){
a[++cnt]=(node){u,v,w,c,head[u]};
head[u]=cnt;
}
inline int read(){
char ch=getchar();int f=0,x=0;
while(ch<'0'||ch>'9'){if(ch=='-') f=1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return f?-x:x;
}
int spfa(){
memset(dis,0x3f3f3f3f,sizeof(dis));
memset(pre,0,sizeof(pre));
memset(vis,0,sizeof(vis));
memset(flow,0,sizeof(flow));
dis[be]=0;vis[be]=1;flow[be]=1e9;
queue<int>q;
q.push(be);
while(!q.empty()){
int now=q.front();
q.pop();
vis[now]=0;
for(int i=head[now];i;i=a[i].nxt){
int v=a[i].v,w=a[i].w,c=a[i].c;
if(dis[v]>dis[now]+c&&w){
dis[v]=dis[now]+c;
pre[v]=i;
flow[v]=min(flow[now],w);
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
return flow[en];
}
void Dinic(){
while(spfa()){
ans1+=flow[en];
ans2+=flow[en]*dis[en];
for(int i=en;i!=be;i=a[pre[i]^1].v){
a[pre[i]].w-=flow[en];
a[pre[i]^1].w+=flow[en];
}
}
return ;
}
signed main()
{
n=read();m=read();be=read();en=read();
for(int i=1;i<=m;i++){
int x=read(),y=read(),w=read(),c=read();
add(x,y,w,c);add(y,x,0,-c);
}
Dinic();
cout<<ans1<<" "<<ans2<<endl;
return 0;
}
对于费用流来讲,还存在有负环的情况,负环跑spfa就炸了,所以就要用消圈算法(待补)。

浙公网安备 33010602011771号