首先说明,此篇博客是我在初学网络流时将看到过的所有很好很经典博客里简单易懂或者精彩的部分摘取组合而成的。每一篇博客侧重点不同,有些讲的比较粗略但是另一些博客很详细,所以我觉得将这些精华聚合在一起还是很重要的,每篇博客我都会标明出处。另外有些地方我也会增加一些我自己的见解,如果有不对的地方,希望各位大佬指出。如果您觉得好的话欢迎转载,但请说明出处:http://blog.csdn.NET/qq_34374664/article/details/52787481
转自:点击打开链接
必须知识:最短路径问题
1.Dijkstra
适用于满足所有权系数大于等于0(lij≥0)的网络最短路问题,能求出起点v1到所有其他点vj的最短距离;
朴素的Dijkstra算法复杂度为O(N^2),堆实现的Dijkstra复杂度为O(NlogN).
2.bellman-ford
适用于有负权系数,但无负回路的有向或无向网络的最短路问题,能求出起点v1到所有其它点 vj的最短距离。bellman-ford算法复杂度为O(V*E)。
3.Floyed
适用于有负权系数,可以求出图上任意两点之间的最短路径。DP思想的算法,时间复杂度为O(N^3);
for ( k= 1; k<= n; k++)
for ( i= 1; i<= n; i++)
if (graph[i][k]!=INF)
for ( j= 1; j<= n; j++)
if (graph[k][j]!=INF && graph[i][k]+graph[k][j]< graph[i][j])
graph[i][j]= graph[i][k]+ graph[k][j];
NO.1 s-t最大流
两大类算法
1.增广路算法
Ford-Fulkerson算法: 残留网络中寻找增加路径
STEP0:置初始可行流。
STEP1:构造原网络的残量网络,在残量网络中找s-t有向路。如果没有,算法得到最大流结束。否则继续下一步。
STEP2:依据残量网络中的s-t有向路写出对应到原网络中的s-t增广路。对于增广路中的前向弧,置s(e)=u(e)- f(e)。对于反向弧,置s(e)=f(e) STEP3:计算crement=min{s(e1),s(e2),…,s(ek)}
STEP4:对于增广路中的前向弧,令f(e)=f(e)+crement;对于其中的反向弧,令f(e)=f(e)-crement,转STEP1。
关键点:寻找可增广路。决定了算法复杂度。
实现:Edmonds-Karp 通过采用了广度优先的搜索策略得以使其复杂度达到O(V*E^2)。
优化—> Dinic算法(*)
Dinic算法的思想是为了减少增广次数,建立一个辅助网络L,L与原网络G具有相同的节点数,但边上的容量有所不同,在L上进行增广,将增广后的流值回写到原网络上,再建立当前网络的辅助网络,如此反复,达到最大流。分层的目的是降低寻找增广路的代价。
算法的时间复杂度为O(V^2*E)。其中m为弧的数目,是多项式算法。邻接表表示图,空间复杂度为O(V+E)。
2.预流推进算法
一般性的push-relabel算法: 时间复杂度达到O(V^2*E)。(*)
relabel-to-front算法->改进
最高标号预流推进:时间复杂度O(V^2*sqrt(E))
NO2. 最小费用最大流
求解最小费用流的步骤和求最大流的步骤几乎完全一致,只是在步骤1时选一条非饱和路时,应选代价和最小的路,即最短路。
步骤1. 选定一条总的单位费用最小的路,即要给定最小费用的初始可行流,而不是包含边数最小的路。
步骤2. 不断重复求最大流的步骤来进行,直到没有饱和路存在为止。然后计算每个路的总费用。
和Edmonds-Karp标号算法几乎一样,因为这两种算法都使用宽度优先搜索来来寻找增广路径,所以复杂度也相同,都是O(V*E^2)。
连续最短路算法 + 线性规划对偶性优化的原始对偶算法(*)
传说中,没见过,据说复杂度是O(V^3)
NO3. 有上下届的最大流和最小流(通过添加点来进行转化)(*)
NO4. 相关图论算法
二分图最大匹配: 加s和t构造最大流
专用算法:hungary算法 O(M*N)
二分图最佳匹配: 加s和t构造最小费用最大流
专用算法:KM算法
朴素的实现方法,时间复杂度为O(n^4)
加上松弛函数O(n^3)
最小路径覆盖:
顶点数-二分图的最大匹配
s-t最小边割集:
最大流最小割定理:最小割等于最大流
普通最小边割集:
Stoer-Wagner Minimum Cut O(n^3)
二分图的最大独立集:
N - 二分图的最大匹配(POJ monthly)girls and boys
反证法证明
普通图的最大独立集是np问题。(*)
转自:点击打开链接
过去听起来高深莫测的网络流算法,现在已飞入寻常百姓家了,对于每一个OIER,网络流是一个神圣的东西(个人见解),但神圣的同时,它并不是那样抽象,最形象的模型就是水流,从长江源点无限的向外流水,而大海(汇点)则在不断地‘喝水’,当然,你也可以不把它想成水,或者是其他一切可以流动的东西。而事实上,有些东西的流动比较流畅,而某些东西可能相对而言比较粘稠,流速更慢,因此,就产生了一个问题,单位时间内的总流量最多多少,这里会根据流速给定单位时间内的流量,这就是最先开启网络流之门的最大流算法,它的解决方式将在后面谈到,再想一下,如果水管是另一个物流公司所有,那么你会根据从哪里运到哪里付出一定的代价, 为了你自己的利润,显然要找一个在运的东西最多的前提下有最小费用的方案,这就引出了下一个问题,最小费用最大流。再引用某牛一句话“当然也有有钱没处花的傻子,去求最大费用最大流”,而事实上,题目会出现这个模型,为了避免你成为傻瓜,现在你要给它一个新的定义:最大收益流,这时的你,变成了物流公司的经理,而客户的路线由你规划,为了你的钱包,最大收益必不可少。
第一部分.概念性问题(基本定理及定义)
对于一些网络流新手来说,有必要知道一些基本定义和基本定理,这些虽然看起来理论价值不大,但是现在的许多网络流描述需要这些专业性的词语,所以还是 有些了解为好。
首先对于图G
G 的流是一个实值函数 f, f (u, v) 表示顶点 u 到顶点 v 的流,它可以为正, 为零,也可以为负,且满足下列三个性质:
1.容量限制:对所有u, v ÎV ,要求 f (u, v) £ c(u, v) 。 反对称性:对所有u, v ÎV ,要求 f (u, v) =- f (v, u) 。
2.流守恒性:对所有u ÎV -{s, t} ,要求 å f (u, v) = 0 。
3.整个流网络 G 的流量 f = å f (s, v) 或 f= å f (u, t) 。
接下来定义各种算法中都要用到的一些东东:
1.残留网络
给定一个流网络G = (V , E) 和流 f,由 f 压得的 G 的残留网络Gf= (V , E f ) ,定义 c f (u, v) 为残留网络G f 中边 (u, v) 的容量。如果弧 (u, v) Î E 或弧 (v, u) Î E ,则 弧 (u, v) Î E f ,且 c f (u, v) = c(u, v) - f (u, v) 。
残留网络又被称为剩余图。
2.点的高度和层次,这是两个相对的概念,高度的定义为到汇点的最短路径长度,而层次则是指到源点的最短路径长度(这里的路径长度按照各个边的长度都为1算),这两个量是在最大流算法中贯穿始末的利器。
接下来引入最大流最小割定理
对了,可能有同学还不知道什么是最小割,在这里提一下
流网络 G = (V , E) 的割 (S ,T ) 将V 划分成 S 和T = V - S 两部分,使得 s Î S ,t ÎT 。定义割 (S ,T ) 的容量为 c(S ,T ),
对 于 最 小 的 c , 它 是 最 小 割 。
3. 最 大 流 最 小 割 定 理
在 流 网 络 中,最 小 割 的 容 量 等 于 最 大 流 的 流 量 。(证 明 再 次 略 过 )
第二部分.最大流的算法
下面步入与实际问题更加接近的算法实现部分,首先给出问题,给定一个流网络,求源到汇在单位时间内的最大流量。
最简单而效率较好的算法 是基于增广路的算法,这类算法在王欣上大牛的论文中有详细介绍,但我仍然想谈谈我的想法,希望能起到抛砖引玉的作用。基于增广路的算法主要有两种:MPLA,Dinic,SAP.其中最简单的是MPLA,最实用最简洁也是最多人用的是Dinia,SAP的范围也很广,加上GAP优化后的效率也让人咋舌,这也是最近SAP大泛滥的原因吧!个人比较喜欢Dinic,数据变态就用最高标号预流推进,SAP用的比较少,当然,用什么算法还是看你自己的感觉吧。有些人认为增广路算法格式低效,于是想出了对于每个节点操作的算法,这类算法以预留推进为顶梁柱,MPM也勉强归入这一类吧。
1.MPLA算法
即最短路径增值算法,可以有一个简单的思想,每次都找一条从源到汇的路径来增广,直到不能增广为止,之中算法的正确性是可以保证的,但效率不尽如人意,有些时候,把事情格式化反而有益,这里的MPLA就是这样,它只在层次图中找增广路,构建出层次图之后,用BFS不断增广,直到当前层次图中不再有增广路,再重新构建层次图,如果汇点不在层次图内,则源汇不再连通,最大流已经求出,否则继续执行增广,如此反复,就可以求出最大流,在程序实现时层次图不用被构建出来,只需要BFS出各点的距离标号,找路径时判断对于f(u,v)是否有d[u]+1=d[v]即可。
如果每建一次层次图成为一个阶段,则在最短路径增值算法中,最多有N个阶段,证明再次略过。
因此在整个算法中,最多有N个阶段,每个阶段构建层次图的BFS时间复杂度为O(m),建N次,因此构建层次图的总时间为O(mn),而在增广过程中,每一次增广至少删除一条边,因此增广m次,加上修改流量的时间,每一阶段的增广时间为O(m*(m+n)),共有N个阶段,所以复杂度为O(n*m*(m+n))=O(nm^2),这也是该算法的时间复杂度。
2.Dinic算法
MPLA虽然简单,但经常会点超时,我们把增广过程中的BFS改成DFS,效率会有比较大的提高么?答案是肯定的,至此我们已经得到了Dinic的算法流程,只是将MPLA的增广改为DFS,就能写出那美妙的Dinic了,同样,分析一下时间,在DFS过程中,会有前进和后退两种情况,最多前进后退N次,而增广路最多找M次,再加上N个阶段,所以Dinic的复杂度就是O(mn^2),事实上,它也确实比MPLA快很多,简洁而比较高效,这也是许多OIER选择Dinic的理由了吧,毕竟,写它可能会节省出较长时间来完成其他题目.
1 2 program dinic(input,output); 3 var 4 f : array[0..1000,0..1000] of longint; 5 number : array[0..1000] of longint; 6 q : array[0..10000] of longint; 7 n,m,ans,s,t : longint; 8 procedure init; 9 var 10 x,y,c : longint; 11 i : longint; 12 begin 13 readln(m,n); 14 s:=1; 15 t:=n; 16 fillchar(f,sizeof(f),0); 17 for i:=1 to m do 18 begin 19 readln(x,y,c); 20 inc(f[x,y],c); 21 end; 22 end; { init } 23 function min(aa,bb :longint ):longint; 24 begin 25 if aathen 26 exit(aa); 27 exit(bb); 28 end; { min } 29 function bfs(): boolean; 30 var 31 head,tail : longint; 32 now,i : longint; 33 begin 34 fillchar(number,sizeof(number),0); 35 head:=0; 36 tail:=1; 37 q[1]:=s; 38 number[s]:=1; 39 while head do 40 begin 41 inc(head); 42 now:=q[head]; 43 for i:=1 to n do 44 if f[now,i]>0 then 45 if number[i]=0 then 46 begin 47 number[i]:=number[now]+1; 48 inc(tail); 49 q[tail]:=i; 50 end; 51 end; 52 if number[t]=0 then 53 exit(false); 54 exit(true); 55 end; { bfs } 56 function dfs(now,flow :longint ):longint; 57 var 58 tmp,i : longint; 59 begin 60 if now=t then 61 exit(flow); 62 for i:=1 to n do 63 if number[i]=number[now]+1 then 64 if f[now,i]>0 then 65 begin 66 tmp:=dfs(i,min(flow,f[now,i])); 67 if tmp<>0 then 68 begin 69 inc(f[i,now],tmp); 70 dec(f[now,i],tmp); 71 exit(tmp); 72 end; 73 end; 74 exit(0); 75 end; { dfs } 76 procedure main; 77 var 78 tmp : longint; 79 begin 80 ans:=0; 81 while bfs() do 82 begin 83 tmp:=dfs(s,maxlongint>>2); 84 while tmp<>0 do 85 begin 86 inc(ans,tmp); 87 tmp:=dfs(s,maxlongint>>2); 88 end; 89 end; 90 writeln(ans); 91 end; { main } 92 begin 93 init; 94 main; 95 end.
3.SAP算法
SAP也是找最短路径来增广的算法,有这样一句话:SAP算法更易理解,实现更简单,效率更高,而也有测试表明,SAP加上重要的GAP优化后,效率仅次于最高标号预流推进算法,因此如果你想背一个模板,SAP是最佳选择。SAP在增光时充分的利用了以前的信息,当按照高度找不到增广路时,它会对节点重新标号,h[i]=min{h[j]}+1(c[i,j]>0),这也是SAP比较核心的思想,而根据这个我们可以发现,当高度出现间隙时,一定不会存在增广路了,算法已经可以结束,因此,这里引入间隙优化(GAP),即出现间隙时结束算法。
在算法实现中,初始标号可以全部置为0,在增广过程中在逐渐提升高度,时间上可能会有常数的增加,但不改变渐进时间复杂度。同时为了简洁,SAP实现时用递归,代码不过80行左右。
1 View Code 2 program sap(input,output); 3 var 4 c : array[0..1000,0..1000] of longint; 5 h,vh : array[0..1000] of longint; 6 flow,n,m,ans : longint; 7 tmpflow : longint; 8 can : boolean; 9 procedure init; 10 var 11 i,j : longint; 12 xx,yy,cc : longint; 13 begin 14 fillchar(c,sizeof(c),0); 15 fillchar(h,sizeof(h),0); 16 ans:=0; 17 readln(m,n); 18 for i:=1 to m do 19 begin 20 readln(xx,yy,cc); 21 inc(c[xx,yy],cc); 22 end; 23 end; { init } 24 procedure dfs(now : longint ); 25 var 26 min,tmp : longint; 27 i : longint; 28 begin 29 min:=n-1; 30 tmp:=tmpflow; 31 if now=n then 32 begin 33 can:=true; 34 inc(ans,tmpflow); 35 exit; 36 end; 37 for i:=1 to n do 38 if c[now,i]>0 then 39 begin 40 if h[i]+1=h[now] then 41 begin 42 if c[now,i]then 43 tmpflow:=c[now,i]; 44 dfs(i); 45 if h[1]>=n then 46 exit; 47 if can then 48 break; 49 tmpflow:=tmp; 50 end; 51 if h[i] then 52 min:=h[i]; 53 end; 54 if not can then 55 begin 56 dec(vh[h[now]]); 57 if vh[h[now]]=0 then 58 h[1]:=n; 59 h[now]:=min+1; 60 inc(vh[h[now]]); 61 end 62 else 63 begin 64 dec(c[now,i],tmpflow); 65 inc(c[i,now],tmpflow); 66 end; 67 end; { dfs } 68 begin 69 init; 70 fillchar(vh,sizeof(vh),0); 71 vh[0]:=n; 72 while h[1] do 73 begin 74 tmpflow:=maxlongint>>2;; 75 can:=false; 76 dfs(1); 77 end; 78 writeln(ans); 79 end.
4.MPM算法
这个算法我还没有实践过,因为它的实现过程比较繁琐,而且时间效率不高,是一个只具有理论价值的算法,这个算法每次都处理单独节点,记每个节点入流和与出流和的最小值作为thoughput(now)(定义在非源汇点),每次先从now向汇推大小为thoughput(now)的流量,在从点now向源点拉大小为thoughput(now)的流量,删除该节点,继续执行直到图中只剩下源汇。时间复杂度为O(n^3),但时间常数较大,时间效率不高。
5.预留推进算法
以上的算法中,基本上都需要从大体上来把握全局,而预留推进算法则是将每一个顶点看作了一个战场,分别对他们进行处理,在处理过程中,存在某些时间不满足流量收支平衡,所以对预先推出的流叫做预流,下面来看算法如何将预流变成最大流的。
预留推进算法有两个主过程,push和relabel,即推进和重标号,它是在模拟水流的过程,一开始先让源的出弧全部饱和,之后随着时间的推移,不断改变顶点的高度,而又规定水流仅能从高处流向低处,所以在模拟过程中,最终会有水流入汇,而之前推出的多余的水则流回了源,那么我们每次处理的是什么节点呢?把当前节点内存有水的节点称为活跃节点,每次对活跃节点执行推流操作,直到该节点不再活跃,如果不能再推流而当前节点仍未活跃节点,就需要对它进行重新标号了,标号后再继续推流,如此重复,直到网络中不再存在活跃节点为止,这时源的流出量就是该网络的最大流。注意,对于活跃节点的定义,不包括源汇,否则你会死的很惨。
朴素的预留推进的效率还过得去,最多进行nm次饱和推进和n^2m次不饱和推进,因此总的时间复杂度为O(mn^2)
事实上,如同增广路算法引入层次图一样,定下一些规则,可以让预留推进算法有更好的时间效率,下面介绍相对而言比较好实现的FIFO预留推进算法,它用一个队列来保存活跃节点,每次从队首取出一个节点进行推进,对一个节点relabel之后把它加到队尾,如此执行,直到队列为空,这样一来,预留推进算法的时间复杂度降为O(n^3),实现的时候,可以加上同样的间隙优化,但注意,出现间隙时不要马上退出,将新标号的的高度置为n+1,继续执行程序,这样会让所有的剩水流回源,满足流量收支平衡,以便最后的统计工作。
1 View Code 2 program preflow(input,output); 3 var 4 f,c : array[0..2000,0..2000] of longint; 5 q,h,vh,e : array[0..2000] of longint; 6 m,n,s,t : longint; 7 flow : longint; 8 procedure init; 9 var 10 i,j : longint; 11 xx,yy,cc : longint; 12 begin 13 readln(m,n); 14 fillchar(f,sizeof(f),0); 15 fillchar(c,sizeof(c),0); 16 fillchar(e,sizeof(e),0); 17 for i:=1 to m do 18 begin 19 readln(xx,yy,cc); 20 inc(c[xx,yy],cc); 21 end; 22 s:=1; 23 t:=n; 24 end; { init } 25 procedure main; 26 var 27 i,j : longint; 28 head,tail : longint; 29 now,tmp,tmph : longint; 30 begin 31 flow:=0; 32 h[s]:=n; 33 head:=0; 34 tail:=0; 35 for i:=1 to n do 36 begin 37 e[i]:=c[s,i]; 38 f[s,i]:=c[s,i]; 39 f[i,s]:=-f[s,i]; 40 if (e[i]>0)and(i<>t) then 41 begin 42 inc(tail); 43 q[tail]:=i; 44 inc(vh[h[i]]); 45 end; 46 end; 47 while headdo 48 begin 49 inc(head); 50 now:=q[head]; 51 for i:=1 to n do 52 if (c[now,i]>f[now,i])and(h[now]=h[i]+1)and(e[now]>0) then 53 begin 54 tmp:=c[now,i]-f[now,i]; 55 if tmp>e[now] then 56 tmp:=e[now]; 57 inc(f[now,i],tmp); 58 dec(f[i,now],tmp); 59 dec(e[now],tmp); 60 inc(e[i],tmp); 61 if (e[i]=tmp)and(i<>s)and(i<>t) then 62 begin 63 inc(tail); 64 q[tail]:=i; 65 end; 66 end; 67 if (e[now]>0)and(now<>s)and(now<>t) then 68 begin 69 tmph:=h[now]; 70 dec(vh[tmph]); 71 h[now]:=$FFFF; 72 for i:=1 to n do 73 if (c[now,i]>f[now,i])and(h[now]>h[i]+1) then 74 h[now]:=h[i]+1; 75 inc(tail); 76 q[tail]:=now; 77 inc(vh[h[now]]); 78 if vh[tmph]=0 then 79 for i:=1 to n do 80 if (h[i]>tmph)and(h[i] then 81 begin 82 dec(vh[h[i]]); 83 h[i]:=n; 84 inc(vh[n]); 85 end; 86 end; 87 end; 88 flow:=0; 89 for i:=1 to n do 90 inc(flow,f[s,i]); 91 end; { main } 92 begin 93 init; 94 main; 95 writeln(flow); 96 end.
下面介绍最后一个,也是编程难度最大,时间表现不同凡响的算法,最高标号预流推进,它的思想是既然水是从高处向低处流的,那么如果从低处开始会做许多重复工作,不如从最高点开始流,留一次就解决问题。再直观一些,引用黑书上的话“让少数的节点聚集大量的盈余,然后通过对这些节点的检查把非饱和推进变成一串连续的饱和推进”。在程序现实现时,用一个表list来储存所有的活跃节点,其中list(h)存储高的为h的活跃节点,同时记录一个level,为最高标号,每次查找时依次从level,level-1……查找,直到找到节点为止,这时从表内删掉这个节点,对它进行Push,Relabel操作,直到该节点不再活跃,继续进行,直到表内不在存在活跃节点。
它的复杂度为O(n^2*m^(1/2)),时间效率很优秀(当然,如果你刻意构造卡预留推进的数据,它比MPLA还慢也是有可能的)。
1 View Code 2 program hign_node_flow(input,output); 3 var 4 c : array[0..1000,0..1000] of longint; {保存原图} 5 f : array[0..1000,0..1000] of longint; {保存当前的预流图} 6 h : array[0..1000] of longint; {保存各个节点当前高度} 7 vh : array[0..1000] of longint; {保存各个高度节点的数量} 8 e : array[0..1100] of longint; {保存各个节点的盈余} 9 level : longint; {当前所有活跃节点的最高高度} 10 l : array[0..1000,0..1000] of longint; {保存活跃节点的表,l[i,0]表示高度为i的活跃节点数,这也是不能用vh数组的原因} 11 n,m,s,t : longint; {节点数,边数,源,汇} 12 listsum : longint; {记录当前在表内的元素个数} 13 flow : longint; {记录流量} 14 inlist : array[0..1000] of boolean; {节点是否在表内} 15 q : array[0..10000] of longint; {用于BFS扩展的队列} 16 procedure init; 17 var 18 i,xx,yy,cc : longint; 19 begin 20 readln(m,n); 21 fillchar(f,sizeof(f),0); 22 fillchar(c,sizeof(c),0); 23 fillchar(e,sizeof(e),0); 24 fillchar(h,sizeof(h),0); 25 fillchar(vh,sizeof(vh),0); 26 for i:=1 to m do 27 begin 28 readln(xx,yy,cc); 29 inc(c[xx,yy],cc);{注意某些情况下有重边,这样处理比较保险} 30 end; 31 s:=1; 32 t:=n; 33 end; { init } 34 procedure insect(now :longint ); {在活跃节点表内插入节点now} 35 begin 36 inlist[now]:=true; {标记now节点在表内} 37 inc(listsum); {表中元素增加1} 38 inc(l[h[now],0]); {高度为h[now]的活跃节点数增加1} 39 l[h[now],l[h[now],0]]:=now; {表中高度为h[now]的第l[h[now],0]个活跃节点为now} 40 if h[now]>level then {更新活跃节点最高高度} 41 level:=h[now]; 42 end; { insect } 43 procedure bfs(); {利用BFS(反向的),求的各个节点的高度} 44 var 45 head,tail,i : longint; 46 begin 47 head:=0; 48 tail:=1; 49 q[1]:=t; 50 h[t]:=1; {汇点的高度为1} 51 while headdo 52 begin 53 inc(head); 54 for i:=1 to n do 55 if c[i,q[head]]>0 then {存在边} 56 if h[i]=0 then {i节点高度没有求出} 57 begin 58 h[i]:=h[q[head]]+1; {求的节点i的高度} 59 inc(tail); 60 q[tail]:=i; 61 end; 62 end; 63 end; { bfs } 64 procedure previous(); {预流推进的预处理} 65 var 66 i : longint; 67 begin 68 for i:=1 to n do 69 begin 70 e[i]:=c[s,i]; {让源点的出弧饱和,则弧的指向点的盈余要改变} 71 f[s,i]:=c[s,i]; {源点出弧饱和} 72 f[i,s]:=-f[s,i]; {反向弧的处理} 73 if (e[i]>0)and(i<>t)and(not inlist[i]) then {节点i成为活跃节点,且不是汇点,没有在表内(其实也不可能在表内)} 74 insect(i); 75 end; 76 h[1]:=n; 77 for i:=1 to n-1 do 78 inc(vh[h[i]]); 79 end; { previous } 80 function find(level :longint ):longint; {传入当前活跃节点集合的最高高度} 81 var 82 i : longint; 83 begin 84 for i:=level downto 1 do {枚举节点集合} 85 if l[i,0]<>0 then {存在节点} 86 begin 87 find:=l[i,l[i,0]]; {返回表的尾元素} 88 inlist[l[i,l[i,0]]]:=false; {返回节点不再表内} 89 dec(l[i,0]); 90 dec(listsum); {表中元素个数减一} 91 while (l[level,0]=0)and(level>0) do {更新level的值} 92 dec(level); 93 exit; 94 end; 95 exit(0); {没有找到节点就返回0} 96 end; { find } 97 procedure push(now :longint ); {推流操作} 98 var 99 i : longint; 100 tmp : longint; 101 begin 102 for i:=1 to n do 103 if (c[now,i]>f[now,i])and(h[now]=h[i]+1)and(e[now]>0) then {如果当前节点有盈余且有出弧不饱和} 104 begin 105 tmp:=c[now,i]-f[now,i]; {tmp记录对弧而言能增广的量} 106 if tmp>e[now] then {这里能增广的量=min(tmp,盈余)} 107 tmp:=e[now]; 108 inc(f[now,i],tmp); {增广操作} 109 dec(f[i,now],tmp); 110 inc(e[i],tmp); {修改节点盈余} 111 dec(e[now],tmp); 112 if (not inlist[i])and(e[i]=tmp)and(i<>t) then {接受流的节点一定成为活跃节点且不再表内,又不是汇点} 113 insect(i); 114 end; 115 end; { push } 116 procedure relable(now : longint ); {重新标号} 117 var 118 i : longint; 119 tmph : longint; 120 begin 121 tmph:=h[now]; {tmph保存未重新标号前now节点的高度} 122 dec(vh[tmph]); {高度为h[now]的节点数减一} 123 h[now]:=$ffff; {高度要取min(j)c[now,j]>0,则先赋值最大} 124 for i:=1 to n do 125 if (c[now,i]>f[now,i])and(h[now]>h[i]+1) then 126 h[now]:=h[i]+1; {更新标号的过程} 127 inc(vh[h[now]]); {新产生节点的高度记录进去} 128 if vh[tmph]=0 then {GAP优化,如果存在间隙,则最大流已求出} 129 for i:=1 to n do 130 if (h[i]>tmph)and(h[i] then {让各个节点均抬高到n} 131 begin 132 dec(vh[h[i]]); 133 h[i]:=n; 134 inc(vh[n]); 135 end; {不能直接退出,否则会无限执行且不满足流量平衡} 136 if (now<>s)and(now<>t) then 137 insect(now);{now经过PUSH过程已经不再活跃节点内了,且一定有盈余,但一定要保证now不是源,汇} 138 end; { ralable } 139 procedure main; 140 var 141 tmp : longint; 142 begin 143 while listsum<>0 do {当表中存在活跃节点时} 144 begin 145 tmp:=find(level); {找到最高标号点} 146 push(tmp); {推进} 147 if e[tmp]>0 then {如果推进后该节点还有盈余} 148 relable(tmp); {重新标号该节点} 149 end; 150 end; { main } 151 procedure print; 152 var 153 i : longint; 154 begin 155 flow:=0; 156 for i:=1 to n do {累加源的出流量} 157 inc(flow,f[s,i]); 158 writeln(flow); 159 end; { print } 160 begin 161 init; 162 bfs(); 163 previous; 164 main; 165 print; 166 end.
小结:
网络流的最大流算法种类繁多,时间效率编程复杂度也不尽相同,对于不同的流网络,选择相应的算法,需要在不断实践中摸索,这也是一个菜鸟到大牛的必经之路。在一般题目中,选用Dinic是一个不错的想法,但当我们发现网络特别稠密时,FIFO的预留推进算法就要派上用场了,而时间比较紧但题目数据弱,我们甚至可以采用搜索找增广路的算法。
提供最大流测试网址:http://hzoi.openjudge.cn/never/1003/
第三部分 最小费用最大流问题
学习了网络流的最大流算法,一定有一种十分兴奋的感觉,那么,就让你借着这股兴奋劲儿,来学习这一章的最小费用流吧。
最小费用流有两种经典的算法,一种是消圈算法,另一种则是最小费用路增广算法。
第一种,消圈算法。如果在一个流网络中求出了一个最大流,但对于一条增广路上的某两个点之间有负权路,那么这个流一定不是最小费用最大流,因为我们可以让一部分流从这条最小费用路流过以减少费用,所以根据这个思想,可以先求出一个最大初始流,然后不断地通过负圈分流以减少费用,直到流网络中不存在负圈为止。
消圈算法的时间复杂度上限为O(nm^2cw),其中c是最大流量,w为非用最大值,而按特定的顺序消圈的时间复杂度为O(nm^2logn)。这里的时间复杂度分析是按照用bellman-ford算法消圈得到的,用SPFA应该可以得到更优的实际运行时间。
第二种,最小费用路增广算法。这里运用了贪心的思想,每次就直接去找s到t的最小费用路来增广,这样得到的结果一定是最小费用,实现较简单,时间复杂度O(mnv),v为最大流量。用SPFA效果极好,但鉴于SPFA的不确定性,有时为了保险,往往运用重新加权技术,具体实践请通过网络或其他途径获得。
最小费用流的东西并不多,事实上是使用最短路径这种特殊的网络流解决了普遍的网络流问题,只要掌握好基础,程序不难写出。
1 View Code 2 program minflow(input,output); 3 var 4 f : array[0..501,0..501] of longint; 5 c : array[0..501,0..501] of longint; 6 min,pre,d : array[0..1000] of longint; 7 q : array[0..2000] of longint; 8 v : array[0..501] of boolean; 9 m,n,s,t : longint; 10 procedure init; 11 var 12 xx,yy,cc,dd : longint; 13 i : longint; 14 begin 15 readln(n,m); 16 fillchar(f,sizeof(f),63); 17 fillchar(c,sizeof(c),0); 18 for i:=1 to n do 19 f[i,i]:=0; 20 for i:=1 to m do 21 begin 22 readln(xx,yy,cc,dd); 23 f[xx,yy]:=dd; 24 c[xx,yy]:=cc; 25 f[yy,xx]:=-dd; 26 end; 27 s:=1; 28 t:=n; 29 end; { init } 30 function argument():boolean; 31 var 32 head,tail : longint; 33 i,now : longint; 34 begin 35 for i:=1 to n do 36 d[i]:=maxlongint>>2; 37 fillchar(v,sizeof(v),false); 38 fillchar(min,sizeof(min),63); 39 head:=0; 40 tail:=1; 41 q[1]:=s; 42 v[1]:=true; 43 d[1]:=0; 44 while headdo 45 begin 46 inc(head); 47 v[q[head]]:=false; 48 now:=q[head]; 49 for i:=1 to n do 50 if c[now,i]<>0 then 51 begin 52 if d[now]+f[now,i] then 53 begin 54 d[i]:=d[now]+f[now,i]; 55 pre[i]:=now; 56 if c[now,i] then 57 min[i]:=c[now,i] 58 else 59 min[i]:=min[now]; 60 if not v[i] then 61 begin 62 inc(tail); 63 q[tail]:=i; 64 v[i]:=true; 65 end; 66 end; 67 end; 68 end; 69 if d[t]=maxlongint>>2 then 70 exit(false); 71 now:=t; 72 while now<>s do 73 begin 74 dec(c[pre[now],now],min[t]); 75 inc(c[now,pre[now]],min[t]); 76 now:=pre[now]; 77 end; 78 end; { argument } 79 procedure main; 80 var 81 ans : longint; 82 begin 83 ans:=0; 84 while argument() do 85 inc(ans,min[t]*d[t]); 86 writeln(ans); 87 end; { main } 88 begin 89 init; 90 main; 91 end.
第四部分 网络流算法的应用
一. 最大流问题。
一般情况下,比较裸的最大流几乎不存在,网络流这种东西考得就是你的构图能力,要不然大家背一背基本算法就都满分了,下面介绍一道比较典型的最大流问题。
问题一:最小路径覆盖问题。
题目链接:http://hzoi.openjudge.cn/never/1004/
最小路径覆盖=|P|-最大匹配数
而最大匹配数可以用匈牙利,也可以用最大流,而两者在这特殊的图中,效率是相同的,而一旦题目有一些变化,网络流可以改改继续用,而匈牙利的局限性较大。
问题二:奶牛航班。
Usaco的赛题,以飞机上的座位作为流量限制,通过实际模型的构建,最终运用最大流算法解决,详解可参考国家集训队论文,具体哪年的忘记了,囧。
最大流实在难已以找到比较有意思的题目,下面进入应用最广泛的最小费用流吧!
二.最小费用流问题(最大收益流问题)
这个问题的模型很多下面就此解析几道例题。
问题一:N方格取数
在一个有m*n 个方格的棋盘中,每个方格中有一个正整数。现要从方格中取数,使任意2 个数所在方格没有公共边,且取出的数的总和最大。
解析:这是一个二分图最大点权独立集问题,就是找出图中一些点,使得这些点之间没有边相连,这些点的权值之和最大。独立集与覆盖集是互补的,求最大点权独立集可以转化为求最小点权覆盖集(最小点权支配集)。最小点权覆盖集问题可以转化为最小割问题解决。
结论:最大点权独立集 = 所有点权 - 最小点权覆盖集 = 所有点权 - 最小割集 = 所有点权 - 网络最大流。
问题还有许多,可以参考网上的网络流与线性规划24题,里面题目比较全面(虽然好多根本用不到网络流)。
最后再提一道题目,说一下最小割的转化建模。
The last问题:黑手党
题目大意:要用最少的人数来切断从A到B的所有路径,每个人只能切断一条边。
分析:显然是一个从A到B的最小割问题,由最大流最小割定理,求A到B 的最大流即可。
结论:网络流问题博大精深,难点在构图,这是一种能力,需要逐渐培养。
总结:关于网络流的介绍到这里也就结束了,但是网络流绝不是仅仅这点东西的,由于个人水平问题,出错或片面的地方还请大牛指正。
参考资料:
[1].国家集训队论文2007 王欣上,浅谈基于分层思想的网络流算法。
[2].国家集训队论文2002,江鹏,从一道题目的解法试坛网络流的构造与算法。
[3].算法艺术与信息学竞赛,刘汝佳,黄亮。
转自:http://www.cnblogs.com/onioncyc/p/6496532.html
网络流24题
【最大流】
网络流:http://m.blog.csdn.net/article/details?id=9401909
网络流本质上是为了解决一类取舍问题,这类取舍问题无法得知最优策略的模式(无法DP),因此通过构造一些带容量的路径表示原题目容量,模拟水流在这些容量之间的取舍,从而可以利用网络流来解决取舍问题。
当前弧优化是因为DFS过程中访问x点时一旦流入量=流出量就退出,所以可以记录下此时正在考虑的弧,下次从此处继续考虑即可。
当前弧之前的弧,不能使流入量-流出量=0,那么一定该弧以及该弧之后的弧中有断裂,那么下次再考虑就没有意义了。
当前弧本身,使流入量-流出量=0,也就是使该点前面的弧中最小的一条断裂了,当前弧以及当前弧连出去之后的弧只是有可能断裂,那么下次就应该从这条当前弧开始考虑。
当前弧优化的效果还是很不错的!
我的写法中直接用flow表示剩余流量,思路和程序结构参考紫书。
记得建反向弧,记得tot=1,不是0!也不是2!
edge开大!
不深究反向弧的意义的话,它就只是flow为0的配套弧,在整个网络流过程都和正常弧没区别。
反向弧的意义无非就是给程序反悔的机会,本来A-->B流了2单位的流,
此时另一路C增广到B时发现这条路的流可以代替一部分(或全部)B之前的流,而A就可以把被代替的这部分流从其他路径D流向终点。
在建了反向弧之后,上行这种发现就表现为C--B--A--D,即C到B后通过反向弧B-A流向D。
这种表现与描述似乎没有很大的相关性,确实等价的,这一切得益于反向弧的存在。
反向弧给程序提供了反悔的机会,本来A-->B流了2,此时发现别的路帮A流一些,而A省下来的流去其他路更好,要实现这种行为就流反向弧即可。
所以,流反向弧不会改变残量网络的总流量,因为这种行为只是在原来的流量中借一部分出来而已。残量网络留下一些反向弧完全没问题而且也是必要的。
bool bfs() { memset(d,-1,sizeof(d)); int head=0,tail=1;q[0]=S,d[S]=0; while(head<tail) { int x=q[head++];if(head>1000)head=0; for(int i=first[x];i;i=e[i].from) if(e[i].flow&&d[e[i].v]==-1) { d[e[i].v]=d[x]+1; q[tail++]=e[i].v;if(tail>1000)tail=0; } } return d[T]!=-1; } int dinic(int x,int a) { if(x==T||a==0)return a; int flow=0,f; for(int& i=cur[x];i;i=e[i].from) if(e[i].flow&&d[e[i].v]==d[x]+1&&(f=dinic(e[i].v,min(a,e[i].flow)))>0) { e[i].flow-=f; e[i^1].flow+=f;//不要写反了 a-=f; flow+=f; if(a==0)break; } return flow; } int ans=0; while(bfs()) { for(int i=s;i<=T;i++)cur[i]=first[i]; ans+=dinic(S,inf); }
【费用流】
关于最小费用最大流算法,与黄学长模板类似。
但是,黄学长标的zkw并不对,按照那篇论文应该叫原始对偶 (Primal-Dual) 算法。
是一种保留spfa的同时改用多路增广的算法,不怕负权,怕负环。
e[i].flow只要走边都必须判断。
spfa 从T开始建立以cost为边权的最短路图,与普通spfa的只是从终点开始搜,边要反过来(e[i^1].cost)
dfs 从S开始多路增广寻找最大流,与dinic的不同:
dinic中走的路要求严格遵循层次图,因此不需要担心走回头路。
dfs中走的路要求遵循最短边(即该边在点x到点T的某条最短路上),因此要记录vis避免走回头路或成环。
还有dfs过程中累加ans=e[i].cost*f(可以保证流f不超容)。
关于费用流的用途:
最大流是让水流在通道内尽可能多的流到终点,为此需要填满所有S到T的通路才停止,从S到T的水流可能有多次分分合合。
而费用并不是简单的“第二关键字”,为许多不同的最大流方案加上代价,以寻找最有价值(代价最小)的流。
dfs自带多路增广,一遍就可以了,不用while。
1.加当前弧优化
2.加SLF优化(spfa)
3.dfs时要回溯
4.tot=1
5.边edge开大!
6.memset数个数,记得int类型*4!
7.spfa要从终点
8.费用流dfs时记得vis数组
bool spfa() { memset(vis,0,T+1); memset(d,0x3f,4*(T+1)); int head=0,tail=1;q[0]=T;vis[T]=1;d[T]=0; while(head!=tail) { int x=q[head++];if(head>3000)head=0; for(int i=first[x];i;i=e[i].from) if(e[i^1].flow&&d[x]+e[i^1].cost<d[e[i].v]) { int y=e[i].v; d[y]=d[x]+e[i^1].cost; if(!vis[y]) { if(d[y]if(head<0)head=3000;q[head]=y;} else{q[tail++]=y;if(tail>3000)tail=0;} vis[y]=1; } } vis[x]=0; } return d[S]<inf; } int dfs(int x,int a) { if(x==T||a==0)return a; vis[x]=1; int flow=0,f; for(int& i=cur[x];i;i=e[i].from) if(!vis[e[i].v]&&d[x]==e[i].cost+d[e[i].v]&&(f=dfs(e[i].v,min(a,e[i].flow)))>0) { e[i].flow-=f; e[i^1].flow+=f; ans+=e[i].cost*f; flow+=f; a-=f; if(a==0)break; } vis[x]=0; return flow; } ans=0; memset(vis,0,T+1); while(spfa()) { for(int i=0;i<=T;i++)cur[i]=first[i]; dfs(S,inf); }
<最小费用流>
流量不固定的s-t最小费用流。如果网络中的费用有正有负,如何求s-t最小费用流?注意,这里的流量并不固定。
解:如果费用都是正的,最小费用流显然是零流;但由于负费用的存在,最短增广路的权值可能是负的,这样增广之后会得到更小的费用;但随着增广的进行,增广路权值逐渐增大,最后变成正数,此时应该停止增广。换句话说,最小费用随着流量增大先减小,后增大,成下凸函数。前面说过,下凸函数求最小值一般使用三分法,但这里可不用这么麻烦,只需在最短增广路费用为正时停止增广即可,三分反而比较慢(想一想,为什么)。需要注意的是,如果一开始不仅有负费用弧,还有负费用圈,必须先用消圈法消去负圈,否则最短增广路算法的前提不成立。当然,如果网络是无环的,则无此问题。——刘汝佳《算法竞赛入门经典——训练指南》
流量不固定(不考虑最大流)的费用流:spfa最后返回时当且仅当最短路总费用对答案有贡献(如<0)时返回。
#include#include #include #include using namespace std; const int maxn=1000,maxm=100010,inf=0x3f3f3f3f; int tot=1,first[maxn],d[maxn],q[10010],S,T,ans,n,cur[maxn]; bool vis[maxn]; struct edge{int from,v,flow,cost;}e[maxm*3]; void insert(int u,int v,int flow,int cost) {tot++;e[tot].v=v;e[tot].flow=flow;e[tot].cost=cost;e[tot].from=first[u];first[u]=tot;//printf("%d %d %d %d\n",u,v,flow,cost); tot++;e[tot].v=u;e[tot].flow=0;e[tot].cost=-cost;e[tot].from=first[v];first[v]=tot;} char c;int s; int read() { s=0; while(!isdigit(c=getchar())); do{s=s*10+c-'0';}while(isdigit(c=getchar())); return s; } bool spfa() { memset(d,-1,4*(T+1)); memset(vis,0,T+1); int head=0,tail=1;q[0]=T;d[T]=0;vis[T]=1; while(head!=tail) { int x=q[head++];if(head>10000)head=0; for(int i=first[x];i;i=e[i].from) if(e[i^1].flow&&d[e[i].v] 1].cost+d[x]) { d[e[i].v]=d[x]+e[i^1].cost; if(!vis[e[i].v]) { vis[e[i].v]=1; if(d[q[head]] if(head<0)head=10000;q[head]=e[i].v;} else{q[tail++]=e[i].v;if(tail>10000)tail=0;} } } vis[x]=0; }//printf("d[S]=%d\n",d[S]); return d[S]>0; } int dfs(int x,int a) { if(x==T||a==0)return a; vis[x]=1; int flow=0,f; for(int& i=cur[x];i;i=e[i].from) if(!vis[e[i].v]&&e[i].flow&&d[x]==e[i].cost+d[e[i].v]&&(f=dfs(e[i].v,min(a,e[i].flow)))>0) { e[i].flow-=f; e[i^1].flow+=f; ans+=f*e[i].cost; a-=f; flow+=f; if(a==0)break; } vis[x]=0; return flow; } int main() { while(scanf("%d",&n)==1) { S=0;T=2*n+1;tot=1; memset(first,0,4*(2*n+3)); for(int i=1;i<=n;i++) { int u; for(int j=1;j<=n;j++) { u=read(); insert(i,j+n,1,u); } } for(int i=1;i<=n;i++)insert(S,i,1,0); for(int i=1;i<=n;i++)insert(i+n,T,1,0); ans=0; while(spfa()) { for(int i=S;i<=T;i++)cur[i]=first[i]; dfs(S,inf); } printf("%d",ans); } return 0; }
<负圈>费用流有负权时就要需要考虑负圈的问题:
原理是消圈定理:流量为f的流是最小费用流当且仅当不存在负费用增广圈。
消圈算法:如果在一个流网络中求出了一个最大流,但对于一条增广路上的某两个点之间有负权路,那么这个流一定不是最小费用最大流,因为我们可以让一部分流从这条最小费用路流过以减少费用,所以根据这个思想,可以先求出一个最大初始流,然后不断地通过负圈分流以减少费用,直到流网络中不存在负圈为止。关键在于负圈内所有边流量同时增加是不会改变总流量的,却会降低总费用。
原图中若存在负圈就必须先用消圈算法消除负圈,每次spfa找负圈后将圈内容量最小边满流(留下反向边)同时费用累加进答案,圈内其它边加上一样的流量,这样总流量是不会变化的但总费用变低,然后接着跑spfa直到没有负圈。
最常用的连续最短路(单路或多路)每次都贪心地取最小费用路径在增广,所以每次增广的都是最优的,不存在增广过程中产生负圈的可能,但要注意它无法处理图一开始就存在负圈的情况。
【有上下界网络流】
有上下界的网络流学习笔记★
有上下界的网络流问题
Dmute
<无源汇上下界可行流>相当于重建图后跑最大流。
如果存在一个可行流,那么一定满足每条边的流量都大于等于流量的下限.因此我们可以令每条边的流量等于流量下限,得到一个初始流,然后建出这个流的残量网络:每条边的流量等于这条边的流量上限与流量下限之差(真正建图)。(不用真的建原反向边)
这个初始流不一定满足流量守恒,因此最终的可行流一定是在这个初始流的基础上增大了一些边的流量使得所有点满足流量守恒.
因此我们考虑在残量网络上求出一个另不满足流量守恒的附加流,使得这个附加流和我们的初始流合并之后满足流量守恒.即:
如果某个点在所有边流量等于下界的初始流中满足流量守恒,那么这个点在附加流中也满足流量守恒,
如果某个点在初始流中的流入量比流出量多x,那么这个点在附加流中的流出量比流入量多x.
如果某个点在初始流中的流入量比流出量少x,那么这个点在附加流中的流出量比流入量少x.
循环流只要求每个点流量守恒,这启发我们建立S、T来引流。(S、T只是引流的作用,如果本来只填下界就流量守恒那甚至不用跑最大流了。)
http://hzwer.com/3356.html
入度>出度时(in[x]>0)时,需要流出去,所以从源向点引一条容量in[x]的边,引诱它流出去。
因为源进来的流量不是真的流量,而出去的流量却是真的,如果平衡了,流出去的就相当于补足原来多的入度。
入度<出度时(in[x]<0)时,需要流进来,所以从点向汇引一条边,引诱它流进来。同上。
为何这样正确?源和汇的作用只是引诱,让入度多的点流向出度多的点,最终实现流量平衡。
由于源汇连出来容量相同(一入度对应一出度,多余量也一定相同),所以如果最终满流就实现了流量平衡,此时源汇就可以无视了。
所以如果最大流==S邻边流量上界之和说明存在可行流,流量为下界流量和(初始流)+最大流(附加流)
1.加边时记录每个点入度和出度(in[i])。
2.根据in[i]建源汇连新边。
最大流
3.检验源点出去的边是否满流。
<有源汇上下界可行流> 模板
要求一个流使得源点的总流出量等于汇点的总流入量,其他的点满足流量守恒,而且每条边的流量满足上界和下界限制。
S流不进,T流不出,S的流出量又和和T的流入量相等,这启发我们从t向s连一条容量inf的边,然后跟上面一样处理(建SS,TT)即可。
判断可行方法与上相同(判断最大流与S邻边),最终t-->s的反向边上存着可行流的总流量(不需要再加原来的下界流量)。
为什么不需要再加原来的下界流量?因为最初根据下界设置基准流量时,基准流量汇集为终点T的in[T]中(入度有余)。
而超级源SS正是把in[T]通过t-->s引向出度有余起点S,所以t-->s的反向边上存着基准流量。
在后来为了流量平衡的增广中,影响到总流量的增广一定会影响到T-->S这条边(很多边的增广只是互补,没有影响t-->s边就不会影响总流量)
从另一方面解释:总流量==S的总流出量==T的总流入量==T-S边的流量。
<有源汇上下界最大流>先和上面一样从t向s连边,然后跑可行流(建SS,TT),判断可行。
再以s、t在残量网络上跑最大流,该最大流就是最终答案!(不用做什么额外处理,因为额外边全部满流了)
要注意的是原循环流流量存在终点往起点的反向弧中了,最后跑最大流时自然会计算进去。
答案就是最后一次最大流。
会不会增广的时候使得一些边不满足流量下限?
不会。因为我们一开始建的就是把流量下限拿出去之后的图,而之后的操作(如最大流)都是符合流量平衡的,极端情况下就都等于流量下界而已(下界符合平衡时)。
<有源汇上下界最小流>先跑有源汇可行流,然后反向跑t-->s的最大流(增加反向边流量相当去减去正向边流量),答案是可行流-最大流。
<上下界最小费用最大流>先找原图的负圈消去,然后跑可行流(不会产生负圈),最后跑s-t/t-s最小费用最大流/最小费用流。
最小费用循环流:找图中的负费用增广圈增广,对于必须满流的边可以设cost为负无穷。
因为循环流没有最大流,若要在循环流中寻找极限值可以尝试附加费用。
【二分图】
S向左侧xi连边,右侧yi向T连边
二分图最大匹配=最小割最大流
最小覆盖=最小割(一条路径覆盖一条边,路径中最小边为最小割,最小割集就切断所有路径)
最大独立集=总结点数(除S.T外所有)-最小覆盖集(最大匹配)——独立集中不能含有边,每个两点匹配都只能选一点,所以跑完最大匹配不剩边了,得到的就是最大独立集(只能删点变小了,跟匹配没关系)
最小路径覆盖:在DAG找尽量少的路径,使每个节点恰好在一条路径上(在且仅在)。
做法:将每个点拆开分别放入xy集合中,如果u到v有一条边,则连边u,v`,然后二分图最大匹配。
初始未匹配ans=n即每个点单独为一条路径,匹配一条说明连了两点,ans-1,所以最终ans=总结点-最大匹配。
二分图带权匹配:要求完美匹配就跑费用流(spfa最长路)即可。
若不要求完美匹配就跑流量不固定的费用流,即spfa时若最短路费用对答案没用贡献(最长路为d[S]≤0)就返回失败。
【最小割】
特性:一条路径必须选一条边(最小)隔断。
定义:把所有顶点分成两个集合S和T=V-S,其中源点s在集合S中,汇点t在集合T中。如果把“起点在S中,终点在T中”的边都删除,就无法从s到达t了,这样的集合划分(S,T)称为一个s-t割,它的容量是起点在S终点在T的所有边的容量和。当不存在增广路时(跑完最大流的残量网络中),S和T分离,跨越集合S和T的边都满载,组成最小割集。
最小割在最大流中一定是满流边,其实就是S到T之间必须经过的边(不管确定与否)。只要找到一条增广路径,就必须经过一条最小割。最小割中的边饱和就再也不能找到增广路径。
一条最小割可以对应多条增广路径,但是一条增广路径只能对应一条最小割(或最小割的可能性)。
求最小割其实就是一条增广路径中容量最小的边,这恰好与最大流的求解是一致的。
每条增广路出一条最小割,该最小割就是增广路中容量最小的边(集)(多条等大时就有多种可能),这条最小割就是把增广路隔断的罪魁祸首。
即使有多种可能,但是定出来的最小割都是必须经过的,实际上它们都是最小割,任意一条割掉都是一样的效果。
两点之间的边(一条或多条)要么同为最小割,要么同不为最小割。
简单求解:再得到最大流f后的残留网络fG中,从开始深度优先遍历(DFS),所有被遍历到的点,即构成点集。注意,虽然最小割中的边都是满流边,但满流边不一定都是最小割中的边。
详细求解:最小割集不是一定的,跑完最大流后缩点(跑一半的边因为反向弧全部被缩),
剩下的满流边(无视未流边)有些边一定是最小割(id[u]==id[S]&&id[T]==id[v]),有些边可能是最小割(id[u]!=id[v])
因为留在外面的边只能是满流边,所以一条S-T路径若包含多条边(边集,即每两个点之间所有边的和,不是重边),这多个边集一定是等大的,都可能是最小割。
实际上只有一条路径上有多个边集的容量等大时,最小割才是不确定的,否则最小割就是容量最小的那条边(集)。
未流边不可能是最小割,因为若该边能连通,其未流的原因必是u->v被其它边(一条或多条)流了,那么最小割就一定是该路径中限制了流量的边,若它自己是最小割就不会被别的边限制流量了。
半流边不可能是最小割,因为限制了流量的边显然不是它。
【最大权闭合(子)图】
S向正权点连边,负权点向T连边,0不管,原边全部转为正无穷(节点权值全部转到了与S、T的连边上)。
hiho 第119周 最大权闭合子图
注意:上文中后面证明中的S集是闭合子图和源点S的集合,T集是其它点和汇点T的集合!
割掉与S相邻的边就是这个点舍弃了S,成为T中的正权点(离开闭合子图);割掉与T相邻的边就是这个点舍弃T,成为S中的负权点(进入闭合子图)。(割的值与答案密切相关)
因为每条路径必须有割,所以对于所有依赖关系要么与T相邻断边(把依赖对象收进来),要么与S相邻断边(把依赖源扔掉)。
最大权闭合子图的权值=所有正权点之和-最小割
可以简单理解为理想可以收入所有正权点,舍弃所有负权点,然而实际上需要扔些正权点,捡一些负权点(即每条路径有一割)。
对于扔掉的正权点,就是减去割去的S邻边权值;对于捡起来的负权点,其实就是加上负权=减去割去的T邻边的权值。
所以权值=正权点之和-割,最小割对应最大权闭合子图。
【最大费用。。。】
1927: [Sdoi2010]星际竞速 最小权路径覆盖
bzoj 3876 支线剧情
PoPoQQQ 网络流 费用流 对偶图
3961: [WF2011]Chips Challenge
2055: 80人环游世界
最小费用循环流(紫书) BZOJ2673: [Wf2011]Chips Challenge 网络流+费用流
紫书例题 网络流24题
最小割 k覆盖 密度子图 上下界
费用-优先级 循环 流X费√
bzoj 3550 线性规划与网络流
分治维护决策单调性
求最小割集
最小割模型在信息学竞赛中的应用
最大流应用
最大最小定理
最小割模型
最大权闭合(子)图 最大权闭合图 hiho 第119周 最大权闭合子图
飞天计划
【网络流技巧】
1.弧cost=a*(flow)^2。
拆边,cap=5时拆成5条边cost=1a,3a,5a,7a,9a。
源于数学理论x^2=1+3+5+...2*x-1(一个数的平方可以表示为连续奇数之和)
【稳定婚姻】
什么是算法:如何寻找稳定的婚姻搭配
每个尚未订婚的男士在他没有求过婚的女士中选一个最喜欢的求婚,然后女士选择向她求婚的最喜欢的一个(包括原未婚夫)订婚。
开始将所有男士入队,然后每次匹配后失配的男士再次从尾端进入队列。
男士优先选择喜欢的结果就是男士最优、女士最差的搭配,反之。
【BZOJ】1458: 士兵占领(上下界网络流)
小结:网络流
【题目】
bzoj网络流
[上下界网络流判定] BZOJ 2406 矩阵
1.【CODEVS】1993 草地排水 最大流
2.【BZOJ】1066: [SCOI2007]蜥蜴 最大流
3.【CODEVS】1034 家园 最大流
4.【费用流】【CODEVS】1227 方格取数2 最小费用最大流(费用流)
5.【CODEVS】1033 蚯蚓的游戏问题 最小费用最大流(费用流)
6.【BZOJ】1834 [ZJOI2010]network 网络扩容 最大流+最小费用最大流(费用流)
最小割模型在信息学竞赛中的应用(ppt) NOI最大获利
最小割模型在信息学竞赛中的应用(论文)
7.【CODEVS】1022 覆盖 二分图最大匹配
题意:n*m图中有若干水塘(1*1不能覆盖),求最多能用多少1*2的矩阵区覆盖(不能重叠)。
题解:对i+j进行奇偶染色,就可以保证相邻两格异色,然后就是二分图最大匹配了。
8.【CODEVS】1922 骑士共存问题 最大独立集
9.网络流24题之最小路径覆盖问题 最小路径覆盖
10.【网络流24题】魔术球问题 最小路径覆盖
11.【BZOJ】1070 [SCOI2007]修车 最小费用最大流(费用流)
12.【有上下界网络流】【ZOJ】2314 Reactor Cooling 有上下界网络流(最大流)
转自:点击打开链接
//其实主要还是自己复习用
//假定读者能够熟练打dinic的板子
有上下界的网络流的核心是”调整”,我们通过一个初始的未必可行的流调整出一个可行流,还可以从可行的未必最大/最小的流调整出最大/最小流.
另一个常用技巧是有源汇的流和无源汇的流(循环流)的转换.除了无源汇可行流的求解,其他有源汇的上下界网络流都要用到这个技巧.
模型:一个网络,求出一个流,使得每条边的流量必须>=Li且<=Hi,每个点必须满足总流入量=总流出量(流量守恒)(这个流的特点是循环往复,无始无终).
这个算法是有上下界网络流算法的基础,只要深刻理解这个算法其他算法也就水到渠成,因此我用大篇幅力图将这个算法的思想和细节阐述清楚.
可行流算法的核心是将一个不满足流量守恒的初始流调整成满足流量守恒的流.
流量守恒,即每个点的总流入量=总流出量
如果存在一个可行流,那么一定满足每条边的流量都大于等于流量的下限.因此我们可以令每条边的流量等于流量下限,得到一个初始流,然后建出这个流的残量网络.(即:每条边的流量等于这条边的流量上限与流量下限之差)这个初始流不一定满足流量守恒,因此最终的可行流一定是在这个初始流的基础上增大了一些边的流量使得所有点满足流量守恒.
因此我们考虑在残量网络上求出一个另不满足流量守恒的附加流,使得这个附加流和我们的初始流合并之后满足流量守恒.即:
如果某个点在所有边流量等于下界的初始流中满足流量守恒,那么这个点在附加流中也满足流量守恒,
如果某个点在初始流中的流入量比流出量多x,那么这个点在附加流中的流出量比流入量多x.
如果某个点在初始流中的流入量比流出量少x,那么这个点在附加流中的流出量比流入量少x.
可以认为附加流中一条从u到v的边上的一个流量代表将原图中u到v的流量增大1
X的数值可以枚举x的所有连边求出.比较方便的写法是开一个数组A[],A[i]表示i在初始流中的流入量-流出量的值,那么A[i]的正负表示流入量和流出量的大小关系,下面就用A[i]表示初始流中i的流入量-流出量
但是dinic算法能够求的是满足流量守恒的有源汇最大流,不能在原网络上直接求一个这样的无源汇且不满足流量守恒的附加流.注意到附加流是在原网络上不满足流量守恒的,这启发我们添加一些原网络之外的边和点,用这些边和点实现”原网络上流量不守恒”的限制.
具体地,如果一个点i在原网络上的附加流中需要满足流入量>流出量(初始流中流入量<流出量,A[i]<0),那么我们需要给多的流入量找一个去处,因此我们建一条从i出发流量=-A[i]的边.如果A[i]>0,也就是我们需要让附加流中的流出量>流入量,我们需要让多的流出量有一个来路,因此我们建一条指向i的流量=A[i]的边.
当然,我们所新建的从i出发的边也要有个去处,指向i的边也要有个来路,因此我们新建一个虚拟源点ss和一个虚拟汇点tt(双写字母是为了和有源汇网络流中的源点s汇点t相区分).新建的指向i的边都从ss出发,从i出发的边都指向tt.一个点要么有一条边指向tt,要么有一条边来自ss,
指向tt的边的总流量上限一定等于ss流出的边的总流量上限,因为每一条边对两个点的A[i]贡献一正一负大小相等,所以全部点的A[i]之和等于0,即小于0的A[i]之和的绝对值=大于0的A[i]之和的绝对值.
如果我们能找到一个流满足新加的边都满流,这个流在原图上的部分就是我们需要的附加流(根据我们的建图方式,“新加的边都满流”和”附加流合并上初始流得到流量平衡的流”是等价的约束条件).
那么怎样找出一个新加的边都满流的流呢?可以发现假如存在这样的方案,这样的流一定是我们所建出的图的ss-tt最大流,所以跑ss到tt的最大流即可.如果最大流的大小等于ss出发的所有边的流量上限之和(此时指向tt的边也一定满流,因为这两部分边的流量上限之和相等).
最后,每条边在可行流中的流量=容量下界+附加流中它的流量(即跑完dinic之后所加反向边的权值).
代码(ZOJ2314 Reactor Cooling)
#include#include #include using namespace std; const int maxn=300,maxm=100000; struct edge{ int to,next,w,num; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w,int num){ lst[len].num=num; lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].num=num; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int vis[maxn],dis[maxn],q[maxn],head,tail,s,t,T; bool bfs(){ vis[s]=++T;dis[s]=1;head=tail=0;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t){ return lim; } int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()) while(x=dfs(s,0x7f7f7f7f))ans+=x; return ans; } int low[maxm],ans[maxm]; int totflow[maxn],n,m; void work(){ memset(totflow,0,sizeof(totflow)); memset(first,-1,sizeof(first));len=0; scanf("%d%d",&n,&m); int u,v,b; s=0;t=n+1; for(int i=1;i<=m;++i){ scanf("%d%d%d%d",&u,&v,&low[i],&b); addedge(u,v,b-low[i],i);totflow[u]-=low[i];totflow[v]+=low[i]; } int sum=0; for(int i=1;i<=n;++i){ if(totflow[i]<0){ addedge(i,t,-totflow[i],0); }else{ sum+=totflow[i]; addedge(s,i,totflow[i],0); } } if(dinic()==sum){ puts("YES"); for(int i=1;i<=n;++i){ for(int pt=first[i];pt!=-1;pt=lst[pt].next){ if(lst[pt].num==0||pt%2==0)continue; ans[lst[pt].num]=lst[pt].w+low[lst[pt].num]; } } for(int i=1;i<=m;++i)printf("%d\n",ans[i]); }else puts("NO"); } int main(){ int tests;scanf("%d",&tests); while(tests--){ work();if(tests)printf("\n"); } return 0; }
2. 有源汇有上下界可行流
模型:现在的网络有一个源点s和汇点t,求出一个流使得源点的总流出量等于汇点的总流入量,其他的点满足流量守恒,而且每条边的流量满足上界和下界限制.
源点和汇点不满足流量守恒,这让我们很难办,因此我们想办法把问题转化成容易处理的每个点都满足流量守恒的无源汇情况.
为了使源汇点满足流量守恒,我们需要有边流入源点s,有边流出汇点t.注意到源点s的流出量等于汇点t的流入量,我们就可以从汇点t向源点s连一条下界为0上界为无穷大的边,相当于把从源点s流出的流量再流回来.在这样的图中套用上面的算法求出一个可行的循环流,拆掉从汇点t到源点s的边就得到一个可行的有源汇流.
这里有一个小问题:最后得到的可行的有源汇流的流量是多少?
可以发现,循环流中一定满足s流出的总流量=流入s的总流量,假定原图中没有边流入s,那么s流出的流量就是t到s的无穷边的流量,也就是s-t可行流的流量.因此我们最后看一下t到s的无穷边的流量(即dinic跑完之后反向边的权值)即可知道原图中有源汇可行流的流量.
代码:这个可行流算法在有源汇有上下界最大流/最小流中都会用到,可以看下面两个算法的代码
3.有源汇有上下界最大流
模型:现在的网络有一个源点s和汇点t,求出一个流使得源点的总流出量等于汇点的总流入量,其他的点满足流量守恒,而且每条边的流量满足上界和下界限制.在这些前提下要求总流量最大.
首先套用上面的算法求出一个有源汇有上下界可行流.此时的流不一定最大.
接下来在残量网络上跑s-t最大流即可.
最终的最大流流量=可行流流量(即t到s的无穷边上跑出的流量)+新增广出的s-t流量
问题:会不会增广的时候使得一些边不满足流量下限?
不会.因为我们一开始建的图就是把大小等于流量下限的流量拿出去之后的残量网络,这些流量根本没有在图中出现.
代码:ZOJ 3229 Shoot The Bullet 东方文花帖 (由于ZOJ的评测插件似乎挂了,并不知道对不对,请谨慎取用)
#include#include #include using namespace std; const int maxn=2005,maxm=100005; const int inf=0x7f7f7f7f; struct edge{ int to,next,w,num; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w,int num){ lst[len].num=num; lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].num=num; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int q[maxn],vis[maxn],dis[maxn],T,s,t,head,tail,ss,tt; bool bfs(){ head=tail=0;vis[s]=++T;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t)return lim; int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()) while(x=dfs(s,inf))ans+=x; return ans; } int totflow[maxn]; void Add(int a,int b,int lo,int hi,int num){ totflow[a]-=lo;totflow[b]+=lo; addedge(a,b,hi-lo,num); } int low[maxm],ans[maxm]; int n,m,tot; void bound_flow(){ int sum=0; for(int i=s;i<=t;++i){ if(totflow[i]<0){ addedge(i,tt,-totflow[i],0); }else{ sum+=totflow[i]; addedge(ss,i,totflow[i],0); } } addedge(t,s,0x7f7f7f7f,0); int tmps=s,tmpt=t; s=ss;t=tt; if(dinic()==sum){ for(int pt=first[ss];pt!=-1;pt=lst[pt].next){ lst[pt].w=lst[pt^1].w=0; } for(int pt=first[tt];pt!=-1;pt=lst[pt].next){ lst[pt].w=lst[pt^1].w=0; } int flow0=lst[len-1].w; lst[len-1].w=lst[len-2].w=0; s=tmps;t=tmpt; printf("%d\n",flow0+dinic()); for(int i=1;i<=m;++i){ for(int pt=first[i+n];pt!=-1;pt=lst[pt].next){ if(lst[pt].num!=0){ ans[lst[pt].num]=lst[pt].w+low[lst[pt].num]; } } } for(int i=1;i<=tot;++i)printf("%d\n",ans[i]); }else{ printf("-1\n"); } } void work(){ s=0;t=n+m+1; ss=n+m+2;tt=n+m+3; memset(first,-1,sizeof(first));len=0; memset(totflow,0,sizeof(totflow)); int x,y; for(int i=1;i<=m;++i){ scanf("%d",&x); Add(n+i,t,x,inf,0); } int l,h; tot=0; for(int i=1;i<=n;++i){ scanf("%d%d",&x,&y); Add(s,i,0,y,0); for(int j=1;j<=x;++j){ ++tot; scanf("%d%d%d",&y,&l,&h); Add(i,n+y+1,l,h,tot);low[tot]=l; } } bound_flow();printf("\n"); } int main(){ while(scanf("%d%d",&n,&m)!=EOF)work(); return 0; }
4.有源汇有上下界最小流
模型:现在的网络有一个源点s和汇点t,求出一个流使得源点的总流出量等于汇点的总流入量,其他的点满足流量守恒,而且每条边的流量满足上界和下界限制.在这些前提下要求总流量最小.
依然是先跑出一个有源汇可行流.这时候的流也不一定是最小的.假如我们能在残量网络上找到一条s-t的路径使得去掉这条路径上的流量之后仍然满足流量下限,我们就可以得到一个更小的流.好像我们并没有什么算法可以”找到尽可能多的能够去除流量的路径”
这时候需要我们再理解一下dinic的反向边.反向边的流量增加等价于正向边的的流量减少.因此我们在残量网络上找出t到s的流就相当于减小了s到t的流,因此我们在跑出可行流的残量网络上跑t-s最大流,用可行流的大小减去这一次t-s最大流的大小就是最小流的大小.(t-s最大流其实是尽量缩减s-t方向的流).
问题:会不会使流量缩减到不满足流量下限?
不会.和有源汇有上下限的最大流一样,我们之前从每条边上拿出了大小等于流量下限的流量构成初始流,这些流量不在我们建出的图中.最极端的情况是缩减到所有边的流量等于流量下限,不会更小了.
代码:bzoj2502 清理雪道
#include#include #include using namespace std; const int maxn=205,maxm=100005; struct edge{ int to,next,w; }lst[maxm];int len=0,first[maxn],_first[maxn]; void addedge(int a,int b,int w){//printf("Add %d %d\n",a,b); lst[len].to=b;lst[len].next=first[a];lst[len].w=w;first[a]=len++; lst[len].to=a;lst[len].next=first[b];lst[len].w=0;first[b]=len++; } int q[maxn],vis[maxn],dis[maxn],head,tail,s,t,T,ss,tt; bool bfs(){ head=tail=0;vis[s]=++T;dis[s]=1;q[tail++]=s; while(head!=tail){ int x=q[head++]; for(int pt=first[x];pt!=-1;pt=lst[pt].next){ if(lst[pt].w&&vis[lst[pt].to]!=T){ vis[lst[pt].to]=T;dis[lst[pt].to]=dis[x]+1;q[tail++]=lst[pt].to; } } } if(vis[t]==T)memcpy(_first,first,sizeof(first)); return vis[t]==T; } int dfs(int x,int lim){ if(x==t)return lim; int flow=0,a; for(int pt=_first[x];pt!=-1;pt=lst[pt].next){ _first[x]=pt; if(lst[pt].w&&dis[lst[pt].to]==dis[x]+1&&(a=dfs(lst[pt].to,min(lst[pt].w,lim-flow)))){ lst[pt].w-=a;lst[pt^1].w+=a;flow+=a; if(flow==lim)return flow; } } return flow; } int dinic(){ int ans=0,x; while(bfs()){ while(x=dfs(s,0x7f7f7f7f))ans+=x; } return ans; } int totflow[maxn]; void del(int x){ for(int pt=first[x];pt!=-1;pt=lst[pt].next)lst[pt].w=lst[pt^1].w=0; } int main(){ int n;scanf("%d",&n); int x,y; memset(first,-1,sizeof(first)); for(int i=1;i<=n;++i){ scanf("%d",&x); for(int j=1;j<=x;++j){ scanf("%d",&y); totflow[i]--;totflow[y]++; addedge(i,y,0x7f7f7f7f); } } s=0;t=n+1;ss=n+2,tt=n+3; for(int i=1;i<=n;++i){ addedge(s,i,0x7f7f7f7f); addedge(i,t,0x7f7f7f7f); } for(int i=1;i<=n;++i){ if(totflow[i]<0){ addedge(i,tt,-totflow[i]); }else{ addedge(ss,i,totflow[i]); } } addedge(t,s,0x7f7f7f7f); int tmps=s,tmpt=t; s=ss;t=tt; dinic(); int flow0=lst[len-1].w; lst[len-1].w=lst[len-2].w=0; del(ss);del(tt); s=tmpt;t=tmps; printf("%d\n",flow0-dinic()); return 0; }
5.有源汇有上下界费用流(待填坑,bzoj3876和Codeforces 708D,不过这两道题都可以用费用流的技巧避开上下界网络流)