网络流基本概念
什么是网络流
在一个有向图上选择一个源点,一个汇点,每一条边上都有一个流量上限(称为容量),即经过这条边的流量不能超过这个上界。同时,除源点和汇点外,所有点的入流和出流都相等,而源点只有流出的流,汇点只有汇入的流。这样的图叫做网络流。
所谓网络或容量网络指的是一个连通的赋权有向图 D= (V,E,C) , 其中V 是该图的顶点集,E是有向边(即弧)集,C是弧上的容量。此外顶点集中包括一个起点和一个终点。网络上的流就是由起点流向终点的可行流,这是定义在网络上的非负函数,它一方面受到容量的限制,另一方面除去起点和终点以外,在所有中途点要求保持流入量和流出量是平衡的。(引自百度百科)
一些定义
源点:只有流出去的点
汇点:只有流进来的点
流量:一条边上流过的流量
容量:一条边上可供流过的最大流量
残量:一条边上的容量-流量
几个基本性质
基本性质一:
对于任何一条流,总有流量<=容量
这是很显然的
基本性质二:
对于任何一个不是源点或汇点的点u,总有
(其中k[i][j]表示i到j的流量)
这个也很显然,即一个点(除源点和汇点)的入流和出流相等
基本性质三:
对于任何一条有向边(u,v),总有
k[u][v]==−k[v][u]
这个看起来并不是很好理解,它的意思就是一条边的反向边上的流是这条边的流的相反数。可以这么想,就是如果有k[u][v]的流从u流向v,也就相当于有-k[v][u]的流从v流向u。这条性质非常重要。
网络流最大流
网络流的最大流算法就是指的一个流量的方案使得网络中流量最大。
网络流最大流的求解
网络流的所有算法都是基于一种增广路的思想,下面首先简要的说一下增广路思想,其基本步骤如下:
1.找到一条从源点到汇点的路径,使得路径上任意一条边的残量>0(注意是小于而不是小于等于,这意味着这条边还可以分配流量),这条路径便称为增广路
2.找到这条路径上最小的F[u][v](我们设F[u][v]表示u->v这条边上的残量即剩余流量),下面记为flow
3.将这条路径上的每一条有向边u->v的残量减去flow,同时对于起反向边v->u的残量加上flow(为什么呢?我们下面再讲)
4.重复上述过程,直到找不出增广路,此时我们就找到了最大流
这个算法是基于增广路定理(Augmenting Path Theorem): 网络达到最大流当且仅当残留网络中没有增广路(由于笔者知识水平不高,暂且不会证明)
举个例子:
为什么要连反向边
我们知道,当我们在寻找增广路的时候,在前面找出的不一定是最优解,如果我们在减去残量网络中正向边的同时将相对应的反向边加上对应的值,我们就相当于可以反悔从这条边流过。
比如说我们现在选择从u流向v一些流量,但是我们后面发现,如果有另外的流量从p流向v,而原来u流过来的流量可以从u->q流走,这样就可以增加总流量,其效果就相当于p->v->u->q,用图表示就是:
图中的蓝色边就是我们首次增广时选择的流量方案,而实际上如果是橘色边的话情况会更优,那么我们可以在v->u之间连一条边容量为u->v减去的容量,那我们在增广p->v->u->q的时候就相当于走了v->u这条"边",而u->v的流量就与v->u的流量相抵消,就成了中间那幅图的样子了。
如果是v->u时的流量不能完全抵消u->v的,那就说明u还可以流一部分流量到v,再从v流出,这样也是允许的。
一个小技巧
虽然说我们已经想明白了为什么要加反向边,但反向边如何具体实现呢?笔者在学习网络流的时候在这里困扰了好久,现在简要的总结在这里。
首先讲一下邻接矩阵的做法,对于G[u][v],如果我们要对其反向边进行处理,直接修改G[v][u]即可。
但有时会出现u->v和v->u同时本来就有边的情况,一种方法是加入一个新点p,使u->v,而v->u变成v->p,p->u。
另一种方法就是使用邻接表,我们把边从0开始编号,每加入一条原图中的边u->v时,加入边v->u流量设为0,那么这时对于编号为i的边u->v,我们就可以知道i^1就是其反向边v->u。
朴素算法的低效之处
虽然说我们已经知道了增广路的实现,但是单纯地这样选择可能会陷入不好的境地,比如说这个经典的例子:
我们一眼可以看出最大流是999(s->v->t)+999(s->u->t),但如果程序采取了不恰当的增广策略:s->v->u->t
我们发现中间会加一条u->v的边
而下一次增广时:
若选择了s->u->v->t
然后就变成
这是个非常低效的过程,并且当图中的999变成更大的数时,这个劣势还会更加明显。
Dinic算法
为了解决我们上面遇到的低效方法,Dinic算法引入了一个叫做分层图的概念。具体就是对于每一个点,我们根据从源点开始的bfs序列,为每一个点分配一个深度,然后我们进行若干遍dfs寻找增广路,每一次由u推出v必须保证v的深度必须是u的深度+1。
//当前弧优化版本
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int maxn=1100;//顶点的个数
const int maxm=1100;//有向边的个数
const int inf=0x3f3f3f3f;
struct node{
int id;
int w;//每一条边的残量
int next;
}side[maxm*2];
int head[maxn],cur[maxn],dep[maxn],cnt,n,m;//dep[]表示分层图深度;cur[i]记录点i之前循环到了哪一条边,用作当前弧优化
int start,end;//源点和汇点
void init()
{
memset(head,-1,sizeof(head));
cnt=0;
}
void add(int x,int y,int d)
{
side[cnt].id=y;
side[cnt].w=d;
side[cnt].next=head[x];
head[x]=cnt++;
}
int dfs(int u,int flow)//u是当前节点,flow是当前容量
{
if(u==end) return flow;//已经到达汇点,返回
for(int &i=cur[u];i!=-1;i=side[i].next)//注意这里的&符号,这样i增加的同时也能改变cur[u]的值,达到记录当前弧的目的
{
int y=side[i].id;
if(dep[y]==dep[u]+1&&side[i].w!=0)//要满足分层图和残量不为0这两个条件
{
int val=dfs(y,min(flow,side[i].w));//向下增广
if(val>0)//增广成功
{
side[i].w-=val;//正向边减
side[i^1].w+=val;//反向边加
return val;
}
}
}
return 0;//没有增广路,返回0
}
int bfs()//用bfs寻找分层图
{
queue<int> q;
memset(dep,0,sizeof(dep));
q.push(start);
dep[start]=1;//源点深度为1
while(q.size())
{
int x=q.front();
q.pop();
for(int i=head[x];i!=-1;i=side[i].next)
{
int y=side[i].id;
if(dep[y]==0&&side[i].w>0)//y还未分配深度,且该残量不为0,则给其分配深度并放入队列
{
dep[y]=dep[x]+1;
q.push(y);
}
}
}
if(dep[end]>0) return 1;
else return 0;//当汇点的深度不存在时,说明不存在分层图,同时也说明不存在增广路
}
int dinic()
{
int ans=0;//记录最大流量
int d;
while(bfs())//能够分层
{
for(int i=1;i<=n;i++)//当前弧优化
cur[i]=head[i];
while(d=dfs(1,inf))//能找到增广路
{
ans+=d;
}
}
return ans;
}
int main()
{
int u,v,c;//从u到v有一个容量为c的边
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&c);
add(u,v,c);//我们可以假设最初的时候有流量,流量为0,那么起始的残量即为容量大小
add(v,u,0);//反向边的残量起始为0
}
scanf("%d%d",&start,&end);
int ans=dinic();
printf("%d\n",ans);
return 0;
}