最近感冒身体状况奇差,直接走堂了上周的3391,OJ也没怎么看(看了也看不进去看进去也不会),乘着灌了一吨药睡醒后结合紫书和课件,参考其他大佬的博客写完这个方面的概念和ADT吧。
1.概念
最大流定义:从$s$(通常规定为$1$)运东西到$t$(通常规定为$n$),最多运多少的问题称为最大流。关注点主要在边上。对一条边$(u,v)$,$c(u,v)$表示它可以运送物品的上限(capacity),$f(u,v)$表示实际运送的流量(flow)。很容易得到(规定)$f(u,v)=-f(v,u)$(对于不直接连接的两点,肯定有$c(u,v)=f(u,v)=0$)。$c,v$之差称为残量,残量网络是原网络的两倍,反向边的作用是用来返回来找最优解。
(https://www.cnblogs.com/dchipnau/archive/2011/09/16/4985966.html)这篇blog说明了反向边的作用,看完后可以参考该博客
最大流问题的目标是把最多的东西从$s$运到$t$,其他结点都是用来中转的(甚至可以不用)。因此对$s$和$t$以外任意点$u$,到其他所有(相邻)结点$v$的流量有 $sumlimits_{(u,v)^{}in E}=0$ (E表示所有edge的集合)(有些$f$是负数)。因为要最多的$s$到$t$,所以,最后到其他任何结点上存在的量为0。(要么经过它最终到$t$,要么压根不经过它)。可以知道最大流的值为$|f|=sumlimits_{(s,v)in E}f(s,v)=sumlimits_{(u,t)in E}f(u,t)$。
2.增广路算法 Edmonds-Karp
核心思想为BFS,EK算法。
从零流(所有边的流量均为0)开始不断增加流量,保持每次增加流量后都满足容量限制,斜对称性,流量平衡。
egin{equation}
left{
egin{array}{lr}
f(u,v)<=c(u,v) \
f(u,v)=-f(v,u) \
forall u!=(s||t), sum limits_{(u,v)in E}f(u,v)=0
end{array}
ight.
end{equation}
算法基于:残量网络中任何一条$s$到$t$的有向道路都对应一条原图中的增广路(可以增加流量的路),只要求出该路中残量最小值$d$,把对应边的流量加上$d$即可。如果残量网络中不存在增广路,则当前流为最大流。
ADT如下:
1 const int INF = 1000000000; 2 const int MAXN = 205; 3 4 struct Edge{ 5 int from, to, cap, flow; 6 Edge(int u, int v, int c, int f):from(u), to(v), cap(c), flow(f){} 7 }; 8 9 struct EK{ 10 int n, m; 11 vector<Edge> edges; //残量网络,边数是两倍 12 vector<int> G[MAXN]; //领接表,G[i][j]表示从i点出来的每条边j在edge中的序号,用来记录和i相关(从i出发)的edges 13 int a[MAXN]; //从s出发到每个点的最小残量, a[t]是整条道路上的最小残量 14 int p[MAXN]; //最短路树上p的入弧编号 15 16 void init(int n){ 17 for(int i=0;i<n;i++){ 18 G[i].clear(); 19 } 20 edges.clear(); 21 } 22 23 void AddEdge(int from, int to, int cap){ 24 edges.push_back(Edge(from, to, cap, 0)); 25 edges.push_back(Edge(to, from, 0, 0)); //反向弧 26 m = edges.size(); //所以m-1是图中最后加入的一条边的编号 27 G[from].push_back(m-2); //图中倒数第二条加入的边是从from发出去的 28 G[to].push_back(m-1); //edges中最后一条边编号为m-1,从to到from 29 } 30 31 int MaxFlow(int s, int t){ 32 int flow = 0;//一开始判断最大流为0 33 for(;;){ 34 memset(a,0,sizeof(a)); 35 queue<int> Q; 36 Q.push(s); 37 a[s] = INF; //到自己的残量为INF所以一定会被取代(如果存在) 38 while(!Q.empty()){ 39 int tmp = Q.front(); 40 Q.pop(); 41 for(int i=0;i<G[tmp].size();i++){ 42 Edge& e = edges[G[tmp][i]]; //用地址符是直接对edges进行操作 43 if(!a[e.to]&&e.cap>e.flow){//存在残量 并且这条路可以走 44 p[e.to] = G[tmp][i]; //这条路径的终点(global编号)从领接表里面找到 45 a[e.to] = min(a[tmp], e.cap-e.flow); //残量取小的 46 Q.push(e.to); 47 } 48 } 49 if(a[t]){ 50 break; 51 } 52 } 53 if(!a[t]){ //此时一定不能再存在增广路了,直接break节约时间 54 break; 55 } 56 for(int u=t;u!=s;u=edges[p[u]].from){ 57 edges[p[u]].flow += a[t]; 58 edges[p[u]^1].flow -= a[t]; //这个异或是干啥用的? 59 //边i的反向边为i^1,^为二进制异或运算符, 60 } 61 flow += a[t]; 62 63 } 64 return flow; 65 } 66 };
3.最小割最大流定理
最大流的值(流量)等于最小割的容量(容量)。
把所有顶点分成两个集合:$S$和$T$,如果把“起点在$S$中,终点在$T$中”的边全部删除,就无法从$s$到$t$了。这样的集合划分称为一个$s,t$割。它的大小定义为:$c(S,T)=sumlimits_{u in S,t in T}c(u,v)$,即所有起点在$S$,终点在$T$的边的容量和。
可以这样理解:从$s$到$t$,必定通过跨越$S$和$T$的边,所以从$s$到$t$的净流量$|f|=f(S,T)=sumlimits_{uin s,vin T}f(u,v)<=sumlimits_{uin S,vin T}c(u,v)=c(S,T)$(因为可能有的边的容量虽有但是已经到达最大流了无法用掉它的容量(记得EK算法每次找路径上$a$最小值))。
结论:EK算法结束时,($a[u]>0$的结点)集合为$S$,其他为$T$,则$(S,T)$是图的$s-t$最小割。