<更新提示>更新提示>
<第一次更新>第一次更新>
<正文>正文>
网络流
网络流的定义
一个流网络\(G=(V,E)\)为一张满足以下条件的有向图:
- 每一条边有一个非负容量,即对于任意\(E\)中的\((u,v)\) , 有\(c(u,v)\geq0\)。
- 如果\(G\)中存在边\((u,v)\) ,那么不存在\((v,u)\) 。我们将图中不存在的边的容量定为\(0\)。
- 图中含有两个特殊节点:源\(s\)与汇\(t\)。
一个流\(f\)是定义在节点二元组\((u\in V,v\in V)\)上的实数函数,满足以下两个个性质:
- 容量限制:对于任意\((u,v)\),满足\(0\leq f(u,v)\leq c(u,v)\)。
- 流量守恒:对于任何非源汇的中间节点\(u\),有\(\sum_{v\in V}f(v,u)=\sum_{v\in V}f(u,v)\)
一个流\(f\)的流量\(|f|\)定义为:\(|f|=\sum_{v\in V}f(s,v)-\sum_{v\in V}f(v,s)\)。
最大流问题
定义
由于图\(G\)中不存在反向边,所以在我们一般只关注流量定义式的前半部分,即:\(|f|=\sum_{v\in V}f(s,v)\)。
那么对于一个网络\(G\),我们称\(\max\{|f|\}=\max\{\sum_{v\in V}f(s,v)\}\)为这张网络的最大流。
预备知识
残量网络:对于网络\(G\),其残量网络\(G_f\)与\(G\)的差别在于每条边的边容量修改为\(G\)中边容量减去当前流的该边流量。具体来说,\(c_f(u,v)=c(u,v)-f(u,v)\)。
另外,残量网络中还包含原图中所有边的反向边,容量等同于正向边在\(f\)中当前流量,用于"反悔"时将流送回起点:\(c_f(v,u)=f(u,v)\)。
简单的理解,残量网络就是原网络流了一股流以后剩下的网络,容量也对应的相减。
而反向边的存在,就给了反悔旧流的机会,也就是说,一股新的流,可以沿反向边流过,代表的涵义就是让之前流这条边的流不再流这条边,而是流向另一个方向。这样我们也就能够理解为什么反向边的容量就是原来的流量了。
增广:设\(f\)为网络\(G\)上的一个流,\(f'\)为残量网络\(G_f\)上的一个流,那么定义增广后的网络为:
\[(f↑ f')(u,v)=\begin{cases}f(u,v)+f'(u,v)-f'(v,u)\ \ \ \ \ if\ (u,v)\in E \\ 0\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ otherwise\end{cases}\]
引理\(1\):增广后网络的流量等于两个流量直接相加,即:\(|f↑ f'|=|f|+|f'|\)。
这个引理为我们之后的最大流算法铺垫了基础,涵义即为我们得知了残量网络上的一次增广操作可以直接由原流量和增广流量计算得到新的网络流量。
增广路:残量网络中从\(s\)到\(t\)的一条简单路径定义为一条增广路,增广路的流量\(c_f(p)\)定义为\(\min\{c_f(u,v)|(u,v)\in p\}\)。
结论\(1\) :增广后流量增加。令\(f_p\)为残量网络\(G_f\)上的一条增广路,则有:\(|f↑ f_p|=|f|+|f_p|>|f|\)。
有了结论\(1\),我们就可以尝试思考如何设计求解网络最大流的算法了。我们得知,只要找到一条增广路,就能增加原网络的流量,并且可以快速计算出新的流量。所以,最初的想法就是不断地在残量网络中找增广路,不断扩大流量。
同时,我们也得知:当残量网络不存在增广路时,原网络的流量即为最大流。
Ford-Fulkerson算法
由预备知识可知,我们有一种最简单的求网络最大流的方法,那就是不断寻找残量网络的增广路,并将增广路的流量累加到答案中。直到残量网络不存在增广路,我们就得到了网络最大流。
于是我们就得到了著名的\(Ford-Fulkerson\)算法,容易写出如下的代码:
function Ford-Fulkerson(G,s,t)
maxflow = 0
for each edge (u,v) belongs to G.E
(u,v).f = 0
while there exists a path p from s to t in the residue network Gf
cf(p) = min { cf(u,v) | (u,v) belongs to p }
maxflow = maxflow + cf(p)
for each edge (u,v) belongs to p
if (u,v) belongs to E
(u,v).f = (u,v).f + cf(p)
else (v,u).f = (v,u).f - cf(p)
return maxflow
而如何找增广路呢?最简单的方法就是\(dfs\),每一次的时间复杂度\(O(m)\)。于是\(Ford-Fulkerson\)算法的时间复杂度就是\(O(m|f_{max}|)\),\(f_{max}\)代表网络\(G\)的最大流。
Edmonds-Karp算法
直接找增广路太暴力了,于是我们想到要对\(FF\)算法进行一些优化。我们发现\(FF\)算法最大的瓶颈就是增广次数太多,那么是否存在一种增广方法,使得增广的次数得到限制呢?答案是肯定的。
我们改造\(FF\)算法,每一次寻找最短路径增广路,就能在不超过\(nm\)次增广后得到网络的最大流。
引理\(2\):按照最短路径增广路增广,每次使所有顶点\(v∈V− \{s,t\}\)到\(s\)的最短距离\(d_v\)增大。
证明:
反证法,假设存在点\(v\in V-\{s,t\}\)使得\(d'_v<d_v\)。那么取\(v\)为第一个成立的节点,并且令\(u\)为\(v\)在最短路径上的前驱节点。由此我们可以得到:
\[d_u=d_v-1,d'_u\geq d_u\]
若边\((u,v)\in E\),则有\(d_v\leq d_u+1\leq d'_u+1=d'_v\),与假设矛盾。
若边\((u,v)\not\in E\),则边\((v,u)\)在增广路上,有\(d_v=d_u-1\leq d'_u-1=d'_v-2\),与假设矛盾。
结论\(2\):按照最短路径增广路增广,每条边最多作为瓶颈边\(\frac{n}{2}-1\)次。
证明:
如果\((u,v)\)是瓶颈边,则\((u,v)\)在\(s\)到\(t\)的增广路上,有\(d_v=d_u+1\)。而增广后,\((u,v)\)将会从残量网络中消失,若边\((u,v)\)重新出现,当且仅当\((v,u)\)在增广路上,而此时又有\(d'_v=d'_u+1\)。
由引理可知,\(d'_v>d_v\),故有\(d'_u\geq d_v+1=d_u+2\)。所以每次重新出现会使最短路的最短距离\(+2\),而最短距离最大为\(n-2\),所以每条边最多作瓶颈边\(\frac{n}{2}-1\)次。
由结论\(2\)我们就能得知,这样增广的总次数不会超过\(nm\)次,如果采用\(bfs\)实现找最短路径增广路,时间复杂度为\(O(nm^2)\),我们称这种最大流算法为\(Edmonds-Karp\)算法。
\(Code:\)
inline bool EdmondsKarp(void)
{
memset( vis , 0x00 , sizeof vis );
queue < int > q; q.push( s );
vis[s] = true , Min[s] = INF;
while ( !q.empty() )
{
int x = q.front(); q.pop();
for (int i=Head[x];i;i=e[i].next)
{
if ( !e[i].val ) continue;
int y = e[i].ver;
if ( vis[y] ) continue;
Min[y] = min( Min[x] , e[i].val );
pre[y] = i;
q.push( y ) , vis[y] = true;
if ( y == t ) return true;
}
}
return false;
}
inline void update(void)
{
int x = t;
while ( x != s )
{
int i = pre[x];
e[i].val -= Min[t];
e[i^1].val += Min[t];
x = e[i^1].ver;
}
maxflow += Min[t];
}
int main(void)
{
input();
while ( EdmondsKarp() ) update();
printf("%d\n",maxflow);
return 0;
}
dinic算法
我们不妨对\(EK\)算法的增广过程进行思考,发现\(EK\)算法的本质就是每次在残量网络上构建最短路树,然后找到一条增广路进行增广。其实,这当中不难发现\(EK\)算法还有优化的余地。
在残量网络构建的最短路树当中,很可能存在多条增广路,而\(EK\)算法却每次只增广一条,就重新构建最短路树了。那么,我们能否设计一个算法,在一次构建最短路树以后实现多路增广,同时处理掉所有的增广路呢?
可行网络:在残量网络上由最短路树构成的子网络我们称为可行网络。
阻塞流:在可行网络上无法在扩充的流称为阻塞流。阻塞流不必要是残量网络的最大流。
利用上述的思路,我们每次构建残量网络的可行网络(最短路树),并用\(dfs\)实现多路增广,直接增广掉可行网络的阻塞流,就能得到一个更高效的算法,我们称之为\(dinic\)算法。
\(Code:\)
inline bool Search(void)
{
memset( d , 0x00 , sizeof d );
memcpy( cur , Head , sizeof Head );
queue < int > q; q.push( s );
d[s] = 1;
while ( !q.empty() )
{
int x = q.front(); q.pop();
for (int i=Head[x];i;i=e[i].next)
{
int y = e[i].ver;
if ( e[i].val && !d[y] )
{
d[y] = d[x] + 1;
q.push( y );
if ( y == t ) return true;
}
}
}
return false;
}
inline int dinic(int x,int flow)
{
if ( !flow || x == t ) return flow; // 剪枝1
int residue = flow;
for (int i=cur[x];i;i=e[i].next)
{
int y = e[i].ver; cur[x] = i; // 剪枝2
if ( e[i].val && d[y] == d[x] + 1 )
{
int k = dinic( y , min( residue , e[i].val ) );
if ( !k ) d[y] = 0; // 剪枝3
e[i].val -= k , e[i^1].val += k;
residue -= k;
if ( !residue ) break; // 剪枝4
}
}
return flow - residue;
}
int main(void)
{
input();
while ( Search() )
maxflow += dinic( s , INF );
printf("%d\n",maxflow);
return 0;
}
可以发现,除了之前提到的算法流程外,我们还在\(dinic\)函数中加了若干剪枝,其中最重要的剪枝为:
\(1.\) 当前弧优化(剪枝\(2\)):不增广同一条边多次,每次记录增广到的最后一条边。
\(2.\) 无效点优化(剪枝\(3\)):对于一个流入流量却没有有效流出任何流量的点,我们不再重复访问。
可以证明,当我们在\(dinic\)算法中加入了如上\(4\)个剪枝后,\(dinic\)算法的时间复杂度为\(O(n^2m)\),实际运行速度则更快。
于是,我们就得到了实现网络最大流最简单而又高效的算法。
<后记>后记>