<更新提示>
<第一次更新>
<正文>
网络流
网络流的定义
一个流网络(G=(V,E))为一张满足以下条件的有向图:
- 每一条边有一个非负容量,即对于任意(E)中的((u,v)) , 有(c(u,v)geq0)。
- 如果(G)中存在边((u,v)) ,那么不存在((v,u)) 。我们将图中不存在的边的容量定为(0)。
- 图中含有两个特殊节点:源(s)与汇(t)。
一个流(f)是定义在节点二元组((uin V,vin V))上的实数函数,满足以下两个个性质:
- 容量限制:对于任意((u,v)),满足(0leq f(u,v)leq c(u,v))。
- 流量守恒:对于任何非源汇的中间节点(u),有(sum_{vin V}f(v,u)=sum_{vin V}f(u,v))
一个流(f)的流量(|f|)定义为:(|f|=sum_{vin V}f(s,v)-sum_{vin V}f(v,s))。
最大流问题
定义
由于图(G)中不存在反向边,所以在我们一般只关注流量定义式的前半部分,即:(|f|=sum_{vin V}f(s,v))。
那么对于一个网络(G),我们称(max{|f|}=max{sum_{vin 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)上的一个流,那么定义增广后的网络为:
引理(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)增大。
证明:
反证法,假设存在点(vin V-{s,t})使得(d'_v<d_v)。那么取(v)为第一个成立的节点,并且令(u)为(v)在最短路径上的前驱节点。由此我们可以得到:
若边((u,v)in E),则有(d_vleq d_u+1leq d'_u+1=d'_v),与假设矛盾。
若边((u,v) otin E),则边((v,u))在增广路上,有(d_v=d_u-1leq 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'_ugeq 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
",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
",maxflow);
return 0;
}
可以发现,除了之前提到的算法流程外,我们还在(dinic)函数中加了若干剪枝,其中最重要的剪枝为:
(1.) 当前弧优化(剪枝(2)):不增广同一条边多次,每次记录增广到的最后一条边。
(2.) 无效点优化(剪枝(3)):对于一个流入流量却没有有效流出任何流量的点,我们不再重复访问。
可以证明,当我们在(dinic)算法中加入了如上(4)个剪枝后,(dinic)算法的时间复杂度为(O(n^2m)),实际运行速度则更快。
于是,我们就得到了实现网络最大流最简单而又高效的算法。
其他增广路算法
我们发现,之前我们提到的三种最大流算法都基于一个最基础的思想:寻找增广路。其实,寻找增广路的算法还有一种:(ISAP)算法。
(ISAP)算法是(SAP)算法的优化,和(EK)算法的思路基本相同,不过只需要进行一次(bfs)。虽然(ISAP)算法的时间复杂度理论上界同样是(O(n^2m)),但是通常来说会有更好的表现。
这里将不再详细介绍(ISAP)算法,具体可以参照这篇博客。
预流推进算法
我们之前一直都在围绕增广路算法进行讨论,其实,网络最大流问题还有另一种思路的算法,叫做预流推进算法。
预留推进算法的思想很简单,首先假设源点(s)有无限多的余流,然后不断将余流推送给相邻的节点,对于其他的点也是同理,直到不能推送了,(t)点的余流即为最大流。
预流推进算法中比较优秀的一种叫做(HLPP)算法,时间复杂度的理论上界为(O(n^2sqrt m)),这里不再详解,可以参照这篇博客。
最小割问题
定义
割:一个对点集(V)的划分称为割,其中点集被划分为两部分(S),(T),源(s)在(S)中,汇(t)在(T)中。对于一个流(f)而言,割((S,T))的流定义为:$$f(S,T)=sum_{uin S}sum_{vin T}f(u,v)-sum_{uin S}sum_{vin T}f(v,u)$$
割((S,T))的容量定义为:[c(S,T)=sum_{uin S}sum_{vin T}c(u,v) ]
那么,对于一个网络(G),我们称(min{c(S,T)})为这个网络的最小割。
预备知识
引理(3):对于任意流(f),任意割的网络流量不变,即(f(S,T)=|f|)。
由流量守恒我们得知这条引理的正确性,而有了这一条引理,我们就可以进一步地推导流与割之间的联系,找到解决最小割问题的算法。
结论(3):任意流(f)的流量不超过任意割的容量,即(|f|leq c(S,T))
证明:
根据结论(3),我们得知了割与流之间的整体关系。我们也发现,流的流量和割的容量可能存在重合点(两值相等),此时最小割等于最大流,那么我们能否证明更一般的结论呢?
最大流最小割定理
最大流最小割定理:对于一个网络(G),以下三个命题总是等价的:
- 流(f)是(G)的最大流
- 当前流(f)的残量网络(G_f)上不存在增广路
- 存在某个割使得(|f|=c(S,T))成立,此时割((S,T))即为网络(G)的最小割
证明:
(Part1 (1)⇒(2):)
反证法,若当前流(f)的残量网络(G_f)上存在增广路,则由结论(1)可知经过这次增广流量(|f|)可以增大,与流(f)是(G)的最大流矛盾。
(Part2 (2)⇒(3):)
构造点集(S)为源(s)在残量网络上能够到达的点的集合,(T=V-S),那么汇(t)在(T)中,进而((S,T))时原网络(G)的一个割。
考虑割((S,T))间的任意点对((u,v)),若((u,v))在原网络中存在,则必然有(f(u,v)=c(u,v)),否则与残量网络(G_f)不存在增广路矛盾,若((v,u))在原网络中存在,则必然有(f(v,u)=0),否则这股流无法回流(由上可知,只存在满流边)。
那么就有:
那么由引理(3)可知:(|f|=c(S,T)),结论成立。
(Part3 (3)⇒(1):)
由结论(3)可知:(|f|leq c(S,T)),那么当等号成立时,所对应的流(f)必然是最大流。
于是,我们就得到了有关最小割问题最重要的定理,那么最小割问题就可以转换为最大流问题解决。
最大权闭合子图问题
最大权闭合子图:给出一张有向图,每个点都有一个点权。在有向图中选取一张权值最大的子图,使得每个节点的后继也都在子图中。
最大权闭合子图问题较难直接求解,但是我们可以将其转换为最小割问题,这是最小割问题最经典的一个运用。
我们建立超级源点(s),(s)连所有正权点,容量为点权,建立超级汇点(t),(t)连所有负权点,容量为点权的相反数。对于原图中的所有边((u,v)),连接((u,v)),容量为(infty)。然后我们在图上求最小割,答案即为正权点权值和(-)最小割。
考虑最小割((S,T)),那么最大权闭合子图的节点即为(S)中的节点。首先,对于一条容量为正无穷的边((u,v)),它一定不会选在最小割中,这就保证了每个点和它的后继被分在同一个集合中。假设一开始所有正权点都在我们选中的最大权闭合子图中,那么:
- 对于一个正权点(u),如果割掉((s,u)),就代表不把这个点选入闭合子图,答案刚好减去(val_u)
- 对于一个负权点(v),如果割掉((v,t)),就代表把这个点选入闭合子图中,答案加上(val_v),刚好减去(-val_v)
至此,最大权闭合子图问题也得到了较好的解决。
(Code:) (()(NOIP2009)植物大战僵尸())
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
const int N = 35 , INF = 0x7f7f7f7f;
struct edge { int ver,val,next; } e[2*N*N*N*N+6*N*N];
int n,m,S,T,s[N][N],c[N][N],indeg[N*N],flag[N*N];
int tot=1,maxflow,Head[N*N],d[N*N],cur[N*N],ans;
pair < int , int > a[N][N][N*N];
vector < int > Link[N*N];
inline int id(int p,int q) { return ( p - 1 ) * m + q; }
inline int read(void)
{
int x = 0 , w = 0; char ch = ' ';
while ( !isdigit(ch) ) w |= ch=='-' , ch = getchar();
while ( isdigit(ch) ) x = x*10 + ch-48 , ch = getchar();
return w ? -x : x;
}
inline void input(void)
{
n = read() , m = read();
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
s[i][j] = read() , c[i][j] = read();
for (int k=1;k<=c[i][j];k++)
{
a[i][j][k].fi = read() , a[i][j][k].se = read();
++ a[i][j][k].fi , ++ a[i][j][k].se;
Link[id(i,j)].push_back
( id(a[i][j][k].fi,a[i][j][k].se) );
indeg[ id(a[i][j][k].fi,a[i][j][k].se) ] ++;
}
if ( j < m )
Link[id(i,j+1)].push_back( id(i,j) ) , indeg[id(i,j)] ++;
}
}
inline void insert(int x,int y,int v)
{
e[++tot] = (edge){y,v,Head[x]} , Head[x] = tot;
e[++tot] = (edge){x,0,Head[y]} , Head[y] = tot;
}
inline void Topsort(void)
{
queue < int > q;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if ( !indeg[id(i,j)] ) q.push( id(i,j) );
while ( !q.empty() )
{
int x = q.front(); q.pop();
flag[x] = true;
for ( auto y : Link[x] )
if ( ! --indeg[y] ) q.push(y);
}
}
inline void build(void)
{
S = n*m+1 , T = n*m+2;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=m;j++)
{
if ( !flag[id(i,j)] ) continue;
if ( s[i][j] > 0 ) ans += s[i][j];
for (int k=1;k<=c[i][j];k++)
if ( flag[id(a[i][j][k].fi,a[i][j][k].se)] )
insert( id(a[i][j][k].fi,a[i][j][k].se) , id(i,j) , INF );
if ( j < m && flag[id(i,j+1)] ) insert( id(i,j) , id(i,j+1) , INF );
if ( s[i][j] > 0 ) insert( S , id(i,j) , s[i][j] );
if ( s[i][j] < 0 ) insert( id(i,j) , T , -s[i][j] );
}
}
}
inline bool Search(void)
{
memset( d , 0 , 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;
int residue = flow;
for (int i=cur[x];i;i=e[i].next)
{
int y = e[i].ver; cur[x] = i;
if ( e[i].val && d[y] == d[x] + 1 )
{
int k = dinic( y , min( residue , e[i].val ) );
if ( !k ) d[y] = 0;
e[i].val -= k , e[i^1].val += k;
residue -= k;
if ( !residue ) break;
}
}
return flow - residue;
}
int main(void)
{
input();
Topsort();
build();
while ( Search() )
maxflow += dinic( S , INF );
printf("%d
",ans-maxflow);
return 0;
}
<后记>