引入: 图论中的一种理论与方法,研究网络上的一类最优化问题 。 很多系统中涉及流量问题,例如公路系统中车流量,网络中的数据信息流,供油管道的油流量等。我们可以将有向图进一步理解为“流网络”(flow network),并利用这样的抽象模型求解有关流量的问题。 一:最大流 1.简介 求解网络流的基本思想就是每次寻找增广路(就是源点到汇点的一条可行路)然后ans+=增广路能流过的流量,更新剩余网络,然后再做增广路,直到做不出增广路。关于网络流入门最难理解的地方就是剩余网络了….为什么在找到一条增广路后…不仅要将每条边的可行流量减去增广路能流过的流量…还要将每条边的反向弧加上增广路能流过的流量.?..原因是在做增广路时可能会阻塞后面的增广路…或者说做增广路本来是有个顺序才能找完最大流的…..但我们是任意找的…为了修正…就每次将流量加在了反向弧上…让后面的流能够进行自我调整…剩余网络的更新(就在原图上更新就可以了) 最大流算法: 首先来看若干个概念 (1)流网络G=(V,E)是一个有向图,其中每条边(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。流网络中有两个特别的顶点:源点s和汇点t。 (2) 对一个流网络G=(V,E),其容量函数为c,源点和汇点分别为s和t。G的流f满足下列三个性质: 容量限制:对所有的u,v∈V,要求f(u,v)<=c(u,v)。 反对称性:对所有的u,v∈V,要求f(u,v)=-f(v,u)。 流守恒性:对所有u∈V-{s,t},要求∑f(u,v)=0 (v∈V)。 容量限制说明了从一个顶点到另一个顶点的网络流不能超过设定的容量,就好像是一个管道只能传输一定容量的水,而不可能超过管道体积的限制;反对称性说明了从顶点u到顶点v的流是其反向流求负所得,就好像是当参考方向固定后,站在不同的方向看,速度一正一负;而流守恒性说明了从非源点或非汇点的顶点出发的点网络流之和为0。一般的最大流问题就是在不违背上述原则的基础上求出从源点s到汇点t的最大的流量值,显然这个流量值应该定义为从源点出发的总流量或是最后聚集到t的总流量,即流f的值定义为|f|=∑f(s,v) (v∈V)。 (3)残留网络 在给定的流网络G=(V,E)中,设f为G中的一个流,并考察一对顶点u,v∈V,在不超过容量c(u,v)的条件下,从u到v之间可以压入的额外网络流量,就是(u,v)的残留容量,就好像某一个管道的水还没有超过管道的上限,那么就这条管道而言,就一定还可以注入更多的水。残留容量的定义为:cf(u,v)=c(u,v)-f(u,v)。而由所有属于G的边的残留容量所构成的带权有向图就是G的残留网络 (4)增广路径 增广路径p为残留网络Gf中从s到t的一条简单路径。根据残留网络的定义,在不违反容量限制的条件下,G中所对应的增广路径上的每条边(u,v)可以容纳从u到v的某额外正网络流。而能够在这条路径上的网络流的最大值一定是p中边的残留容量的最小值。 1:EK算法 EK算法基于一个基本的方法:Ford-Fulkerson方法 即增广路方法 简称FF方法 增广路方法是很多网络流算法的基础 一般都在残留网络中实现 其思路是每次找出一条从源到汇的能够增加流的路径 调整流值和残留网络 不断调整直到没有增广路为止 FF方法的基础是增广路定理(Augmenting Path Theorem):网络达到最大流当且仅当残留网络中没有增广路 EK算法的思路非常的简单,就是一直找增广路径(BFS),假如有,记录增广路的最小值k,ans+=k ,并更新残量网络(要加反向弧) EK算法的时间复杂度是O(m^2n) 最多增广mn次,bfs复杂度O(m) 下面来段代码模板:
#include <iostream> #include <queue> #include <cstring> using namespace std; #define arraysize 201 int maxData = 0x7fffffff; int capacity[arraysize][arraysize]; //记录残留网络的容量 int flow[arraysize]; //标记从源点到当前节点实际还剩多少流量可用 int PRe[arraysize]; //标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中 int n,m; queue<int> myqueue; int BFS(int src,int des) { int i,j; while(!myqueue.empty()) myqueue.pop(); //队列清空 for(i=1;i<m+1;++i) pre[i]=-1; //初始化前驱 pre[src]=0; //源点的前驱是0 flow[src]= maxData; //源点具有无限大的流量 myqueue.push(src); //进队 while(!myqueue.empty()) //队列不为空 { int index=myqueue.front(); //获取队首 myqueue.pop(); if(index == des) break; //找到了增广路径 [终点] for(i=1;i<=m;++i) { if(i!=src && capacity[index][i]>0 && pre[i]==-1) { //不是源点 有容量 不在队列 pre[i] = index; //记录前驱 flow[i] = min(capacity[index][i],flow[index]); //关键:迭代的找到增量 //printf("ind:%d i:%d flowi:%d cap:%d flowind:%d/n",index,i,flow[i],capacity[index][i],flow[index]); myqueue.push(i); } } } if(pre[des]==-1) return -1; //残留图中不再存在增广路径 else return flow[des]; //返回这条路的流量 } int maxFlow(int src,int des) { int increasement= 0; int sumflow = 0; while((increasement=BFS(src,des))!=-1) { int k = des; //利用前驱寻找路径 while(k!=src) { int last = pre[k]; capacity[last][k] -= increasement; //改变正向边的容量 capacity[k][last] += increasement; //改变反向边的容量[关键] k = last; } sumflow += increasement; } return sumflow; } int main() { int i,j; int start,end,ci; while(scanf("%d%d",&n,&m)!=EOF) { memset(capacity,0,sizeof(capacity)); memset(flow,0,sizeof(flow)); for(i=0;i<n;++i) { scanf("%d%d%d",&start,&end,&ci); if(start==end) //考虑起点终点相同的情况 continue; capacity[start][end] +=ci; //此处注意可能出现多条同一起点终点的情况 } cout<<maxFlow(1,m)<<endl; } return 0; }2:Dinic算法
1、初始化流量,计算出剩余图 2、一次bfs对顶点标号,计算出层次图,如果汇点不在层次图内,那么算法结束 3、一次dfs过程找增广 4、转步骤 2 顶点u的层次:level(u)=在剩余图中从源点到u所经过的最少边数 层次图:对于剩余图中的任意一条边(a,b), 当且仅当level(a)+1=level(b)时,(a,b)是层次图中的边 复杂度O(n2*m) 程序简短 对于较大规模的数据实际速度很快 代码:
#include<cstdio> #include<cstring> #include<cmath> #include<iostream>#include<algorithm>#include<queue> #define CL(a,num) memset(a,num,sizeof(a)); #define eps 1e-12 #define inf 0x7fffffff const double pi = acos(-1.0); typedef __int64 ll; const int maxn = 300 ; using namespace std; int n , m; int flow[maxn][maxn],dis[maxn] ;//dis[i],表示 到 原点 s 的 层数 int bfs(){ CL(dis,-1); dis[1] = 0 ; queue<int>que; que.push(1); while(!que.empty()) { int k = que.front();que.pop() ; for(int i=1;i<=n;i++) { if(flow[k][i]>0&&dis[i]==-1)//可以到达&&还没有访问 { dis[i]=dis[k]+1;//建立dfs轨迹 que.push(i) ; } } } if(dis[n]>0) return 1;//有增逛路 return 0 ; } int dfs(int x,int mx){ int i,a; if(x==n) return mx; for(i=1;i<= n;i++) { if(dis[i]==dis[x]+1&&flow[x][i]>0) { if(a=dfs(i,min(mx,flow[x][i])))//流量与容量的最小值 { //printf("x:%d i:%d a:%d/n",x,i,a); flow[x][i]-=a; flow[i][x]+=a; return a ; } } } return 0; } int main() { int i ,s,e,c; while(scanf("%d%d",&m,&n)!=EOF) { CL(flow,0); for(i=0;i<m;i++) { scanf("%d%d%d",&s,&e,&c); flow[s][e] += c; } int ans=0; int res; while(bfs()) { while(res=dfs(1,inf)) ans+= res; } printf("%d/n",ans); } }3:ISAP 之前的两种算法是SAP算法,这里介绍一下ISAP算法 算法基于这样的一个事实:每次增广之后,任意结点到汇点(在残余网络中)的最短距离都不会减小。这样,我们可以利用d[i[表示结点i到汇点的距离的下界。然后再增广过程当中不断地修改这个下界。增广的时候和Dinic算法类似,只允许沿着d[i]==d[j]+1的弧(i,j)走。 不难证明,d[i]满足两个条件:(1)d[t]=0;(2)对任意的弧(i,j) d[i]≤d[j]+1。因为最坏的情况就是s到t是一条链,此时等号成立。因此,当d[s]≥n时,残余网络中不存在s-t路。 那么与Dinic算法类似,事先逆向bfs,找增广的过程就是沿着“允许弧”(即满足f< c且d[i]==d[j]+1的弧)往前走。(称为“前进”)。如果向前走不动了,那么就要考虑原路返回(称为“撤退”)。此时把d[i]修改为min{d[j]}+1即可。因为要满足d函数的条件(2)。修改后,原来的i值的个数就减少一个,而新i值的个数多一个。在程序中,用num数组来保存所有距离的个数,当把距离值从x修改为y时,num[x]–-,num[y]++即可,然后检查num[x]是否为0,如果是0,那么s-t不连通,算法终止。原因显而易见:比如s-t的距离是3,如果距离为2的情况都已经没了,更别提走到距离为1的点了。这就是所谓的“gap优化”。 通过之前的分析,在数据结构方面,该算法只比Dinic算法的数据结构多了两个数组:用于记录父边以便于撤退的数组p,以及标记距离个数的数组num。增广的时候分为两步,第一步逆推求出可改进量a(即残余量的最小值);第二步再逆推一遍,进行增广。主过程中,x走到汇点时增广。 在下面代码中,由于我们是连续存的正向边和反向边,如0和1,2和3,等等。而他们分别与1异或后可以得到对方(二进制最后一位变反,其他位不变),所以我们在更新反向边时用到了这一点。 代码:
int source; // 源点int sink; // 汇点int p[max_nodes]; // 可增广路上的上一条弧的编号int num[max_nodes]; // 和 t 的最短距离等于 i 的节点数量int cur[max_nodes]; // 当前弧下标int d[max_nodes]; // 残量网络中节点 i 到汇点 t 的最短距离bool visited[max_nodes];bool bfs(){ memset(visited, 0, sizeof(visited)); queue<int> Q; Q.push(sink);visited[sink] = 1;d[sink] = 0; while (!Q.empty()) { int u = Q.front(); Q.pop(); for (iterator_t ix = G[u].begin(); ix != G[u].end(); ++ix) { Edge &e = edges[(*ix)^1]; if (!visited[e.from] && e.capacity > e.flow) visited[e.from] = true,d[e.from] = d[u] + 1,Q.push(e.from); } } return visited[source];}int augment(){ int u = sink, df = __inf;// 从汇点到源点通过 p 追踪增广路径, df 为一路上最小的残量 while (u != source) { Edge &e = edges[p[u]]; df = min(df, e.capacity - e.flow); u = edges[p[u]].from; } u = sink;// 从汇点到源点更新流量 while (u != source) { edges[p[u]].flow += df; edges[p[u]^1].flow -= df; u = edges[p[u]].from; } return df;}int max_flow(){ int flow = 0; bfs(); memset(num, 0, sizeof(num)); for (int i = 0; i < num_nodes; i++) num[d[i]]++; int u = source; memset(cur, 0, sizeof(cur)); while (d[source] < num_nodes) { if (u == sink) { flow += augment(); u = source; } bool advanced = false; for (int i = cur[u]; i < G[u].size(); i++) { Edge& e = edges[G[u][i]]; if (e.capacity > e.flow && d[u] == d[e.to] + 1) { advanced = true; p[e.to] = G[u][i]; cur[u] = i; u = e.to; break; } } if (!advanced) { // retreat int m = num_nodes - 1; for (iterator_t ix = G[u].begin(); ix != G[u].end(); ++ix) if (edges[*ix].capacity > edges[*ix].flow) m = min(m, d[edges[*ix].to]); if (--num[d[u]] == 0) break; // gap 优化 num[d[u] = m+1]++; cur[u] = 0; if (u != source) u = edges[p[u]].from; } } return flow; }二:最小割 1:定义 割:流网络图G=(V,E)的一个划分,记作[S,T],将点集[V]划分为S和T两部分,且使得s属于S,t属于T,S+T=V。 最小割:一个网络的最小割也就是该网络中容量最小的割。 最大流最小割定理:流网络图G=(V,E)的最大流大小等于其最小割容量。 带权图的割就是割集中边或者有向边的权和 通俗的理解一下: 割集好比是一个恐怖分子 把你家和自来水厂之间的水管网络砍断了一些 然后自来水厂无论怎么放水 水都只能从水管断口哗哗流走了 你家就停水了 割的大小应该是恐怖分子应该关心的事 毕竟细管子好割一些 而最小割花的力气最小 下面介绍网络流理论中一个最为重要的定理 最大流最小割定理:网络的最大流等于最小割 具体的证明分三部分 1.任意一个流都小于等于任意一个割 由于容量限制 每一根的被砍的水管子流出的水流量都小于管子的容量 每一根被砍的水管的水本来都要到你家的 现在流到外面 加起来得到的流量还是等于原来的流 管子的容量加起来就是割 所以流小于等于割 由于上面的流和割都是任意构造的 所以任意一个流小于任意一个割
2.构造出一个流等于一个割 当达到最大流时 根据增广路定理 残留网络中s到t已经没有通路了 否则还能继续增广 我们把s能到的的点集设为S 不能到的点集为T 构造出一个割集C[S,T] S到T的边必然满流 否则就能继续增广 这些满流边的流量和就是当前的流即最大流 把这些满流边作为割 就构造出了一个和最大流相等的割
3.最大流等于最小割 设相等的流和割分别为Fm和Cm 则因为任意一个流小于等于任意一个割 任意F≤Fm=Cm≤任意C 定理说明完成
所以,我们就可以把最小割转化为最大流,使用上面介绍的三种算法。
三:最大权闭合图、最大密度子图、混合图欧拉回路
最大权闭合图:(此部分转载) 这里闭合图的概念就很好引出了。在一个图中,我们选取一些点构成集合,记为V,且集合中的出边(即集合中的点的向外连出的弧),所指向的终点(弧头)也在V中,则我们称V为闭合图。最大权闭合图即在所有闭合图中,集合中点的权值之和最大的V,我们称V为最大权闭合图。 上图中闭合图有
{5}、{2,5}、{4,5} {2,4,5}、{3,4,5} {1,2,3,4,5}、{1,2,4,5}最大权闭合图为{3,4,5}。
针对本题而言,我们将实验与仪器间连一条有向边,实验为起点(弧尾),仪器为终点(弧头)。则如果我们选择一个闭合图,那么这个闭合图中包含的实验所需要的仪器也最这个闭合图里。而最大权闭合图即为题目的解。
了解了最大权闭合图的概念,接下来我们就需要知道如何求最大权闭合图。
首先我们将其转化为一个网络(现在不要问为什么,接下来会证明用网络可以求解)。构造一个源点S,汇点T。我们将S与所有权值为正的点连一条容量为其权值的边,将所有权值为负的点与T连一条容量为其权值的绝对值的边,原来的边将其容量定为正无穷。
上图即被转化为如左图网络。
首先引入结论,最小割所产生的两个集合中,其源点S所在集合(除去S)为最大权闭合图,接下来我们来说明一些结论。
证明:最小割为简单割。 1:引入一下简单割的概念:割集的每条边都与S或T关联。(请下面阅读时一定分清最小割与简单割,容易混淆):那么为什么最小割是简单割呢?因为除S和T之外的点间的边的容量是正无穷,最小割的容量不可能为正无穷。所以,得证。
2:证明网络中的简单割与原图中闭合图存在一一对应的关系。(即所有闭合图都是简单割,简单割也必定是一个闭合图)。 证明闭合图是简单割:如果闭合图不是简单割(反证法)。那么说明有一条边是容量为正无穷的边,则说明闭合图中有一条出边的终点不在闭合图中,矛盾。 证明简单割是闭合图:因为简单割不含正无穷的边,所以不含有连向另一个集合(除T)的点,所以其出边的终点都在简单割中,满足闭合图定义。得正。 3:证明最小割所产生的两个集合中,其源点S所在集合(除去S)为最大权闭合图。 首先我们记一个简单割的容量为C,且S所在集合为N,T所在集合为M。 则C=M中所有权值为正的点的权值(即S与M中点相连的边的容量)+N中所有权值为负的点权值的绝对值(即N中点与T中点相连边的容量)。记(C=x1+y1);(很好理解,不理解画一个图或想象一下就明白了)。 我们记N这个闭合图的权值和为W。 则W=N中权值为正的点的权值-N中权值为负的点的权值的绝对值。记(W=x2-y2); 则W+C=x1+y1+x2-y2。 因为明显y1=y2,所以W+C=x1+x2; x1为M中所有权值为正的点的权值,x2为N中权值为正的点的权值。 所以x1+x2=所有权值为正的点的权值之和(记为TOT). 所以我们得到W+C=TOT.整理一下W=TOT-C. 到这里我们就得到了闭合图的权值与简单割的容量的关系。 因为TOT为定值,所以我们欲使W最大,即C最小,即此时这个简单割为最小割,此时闭合图为其源点S所在集合(除去S)。得正。 至此,我们就将最大权闭合图问题转化为了求最小割的问题。求最小割用最小割容量=最大流,即可将问题转化为求最大流的问题。 【持续更新中…】
新闻热点
疑难解答