2016.8.9最大流
来源:互联网 发布:ubuntu 压缩 编辑:程序博客网 时间:2024/06/06 10:55
现在有一道题目,大概是这样的,有N个城市和M条将它们相连的公路,每条公路都有单位时间内的车流量限制,求单位时间内从城市1到N的车流量最大为多少。
看这一题可以转换成在一幅图找1到N的最大流量,也就是最大流问题。1是源点,N是汇点,每条公路的车流限制就是每条边的容量。首先想到的就是用搜索,每次选流量最大的一条路径流,直到整幅图再也无法找到源点流向汇点的路径。但是否是正确的呢?
看一个例子:
用上面的方法,先找到S->1->2->T这条路径,然后就没有路径可以从S->T,所以计算出的最大流为3。
但细心分析,肯定会发现这种方法是不对的,因为可以先S->1->T,流量为2,然后S->2->T,流量为2,最后S->1->2->T,流量为1(前面已经用了,S->1剩下1的流量,2->T剩下1的流量,所以这条路径最多流1),计算出的最大流为5。说明上面的方法是不对的,那要怎么做呢。
增广路算法
介绍这个算法之前,先介绍几个基本概念:
残余网络:也就是每条边余剩容量和点组成的网络(原网络每条边减掉已用容量(减到0就删去))。
层次网络:就是将残余网络按到源点的最短距离分层(或标记)(与边的容量无关)。
增广路算法的总体思想就是在原有边的基础上,增加一条反向弧(不能与跟原有边方向相反的边一起储存,应该分开),它的初始容量为0,如果原有边的容量减少k,则这条反向弧的容量增加k,如果反向弧的的容量减少k,那么原有边的容量也增加k,也就是说,反向弧余剩容量+原有边余剩容量=原有边原有容量,反向弧的容量就是这条边流出的流量,这样做,就可以使流出去的流量流回来,即“反悔”。加了反向弧后,将它看成一条与其他边一样的边,再用上面的方法进行搜索。
具体实现及证明看下图:
这还是刚刚那幅图,加上了反向弧,A/B,A指最大容量,B指余剩容量。与刚刚的方法类似,先找出一条路径,处理后的情况是这样的:
这次的流量是3,路径是S->1->2->T,由图可以看出,每条反向弧的容量都增加了3。
如果没有反向弧,那么算法到这里就已经结束了,当然答案也是错误的。如果有反向弧,那么从源点到汇点仍然是有路径的,如下图:
走反向弧的意思就是:本来1->2流过来3个流量,现在我将两个流量退回1点,再从1->T,使从S->2来的流量可以通过2->T流到汇点。但如何证明可以向回流呢?
其实上面的做法可以等价于S->1->2->T只流一个流量,剩下本来要从这条路径流到汇点的2个流量,通过S->1->T这条路流到汇点。S->2->T则流两个,这是一种通俗的证明方法,可能不太准确,详细的可以在网上搜。
按照上面的思想,可以很容易想出一种方法,就是不断增广,直到再也无法增广(没有路径从源点到达汇点),则求出了最大流。
代码:
#include <iostream>#include <fstream>#include <algorithm>#include <string.h>#include <cstring>#include <math.h>#include <cmath>using namespace std;const int MAXV = 10000006, MAXN = 406;int head[MAXN],v[MAXN],node[MAXN],next[MAXN];bool f[MAXN];int n,m,x,y,z,Min,ans=0,fp=0;bool dfs(int k) //使用dfs增广 { if (k==n) {ans+=Min;return 1;} //找到一条路径就结束搜索 f[k]=1; for (int i=head[k];i!=-1;i=next[i]) { if (v[i]==0 || f[node[i]]) continue; int mx=Min; Min=min(Min,v[i]); //记录路径上的边的最小容量(即最后能够到达汇点的最大流量) if (dfs(node[i])) { v[i]-=Min; v[i^1]+=Min; //将自己的对应边(反向弧对原有边,连续下标储存,可以直接^1) return 1;//找到一条路径就结束搜索 } Min=mx; } return 0;}void _insert(int x,int y,int z) //邻接链表 { node[fp]=y; next[fp]=head[x]; head[x]=fp; v[fp]=z; fp++;}int main(){ while (~scanf("%d%d",&m,&n)) { ans=0;fp=0; memset(head,-1,sizeof head); for (int i=0;i<m;i++) { scanf("%d%d%d",&x,&y,&z); _insert(x,y,z); //添加原有边入图 _insert(y,x,0); //添加反向弧入图 } do { memset(f,0,sizeof f); Min=MAXV; }while (dfs(1)); //增广至无法增广 printf("%d\n",ans); } return 0;}
最短增广路算法
其实最短增广路算法只是比上一种多了一个“最短”,也就是保证,每次走的路径都要保证是当前最短的,要保证最短,可以用最短路建立一个层次网络(见上面定义),每次保证只从第k层走到第k+1层,由于源点一定是在第0层,汇点的层数就是源点到汇点的最短距离,所以保证走的是最短路,当在这一阶段的分层图中再也找不到路径,那么就需要再次分层(可能增加了许多反向弧,减少了原有边),最后当发现图中再也没有路径可以从源点到达汇点(不一定是最短路,在最短路时可以判断),那么最短增广路算法就结束了。
在最短增广路中,最多建立n个层次网络,每个层次网络用BFS一次遍历即可得到。一次BFS的复杂度为O(m),所以建层次图的总复杂度为O(n*m)。
每增广一次,层次网络中必定有一条边会被删除。层次网络中最多有m条边,所以认为最多可以增广m次。在最短增广路算法中,用DFS来增广,一次增广的复杂度为O(n+m),其中O(m)为DFS的花费,O(n)为修改流量的花费。所以在每一阶段寻找增广路的复杂度为O(m*(m+n)) = O(m*m)。因此n个阶段寻找增广路的算法总复杂度为O(n*m*m)。
两者之和为O(n*m*m)。
Dinic算法
dinic算法其实也是增广路算法的优化,它的思想就是,将本来每次只能增广一次DFS改进,使得它可以实现多次增广。
在上面的算法中,DFS只要找到一条路径,就会结束搜索,这很明显没有利用好时间,下一次又要从头开始搜索,而dinic就是改进了这一方面。
对于每一个点,本来是只找一条边流出,现在是找多条边,尽量将前面的边能流过来的流完,每个点都如此,这样就能一次DFS就完成这一阶段层次网络增广,再用BFS进行分层,直到图中再也没有路径可以从源点到达汇点,算法结束。
dinic算法总是寻找最短的増广路并沿着它增广,増广路的长度不会在增广过程中改变,则当无法增广时,说明分层图上没有可以增广的路线了,这有两种情况,第一,已经求出了最大流,第二,可能存在长一些的増广路可以继续增广,因此,继续bfs构造分层网络。每次完成后最短増广路长度+1,由于最短路﹤n,则最对重复n-1次bfs就可完成了。
在每一阶段,最多增广m次,每次修改流量的费用为O(n)。而一次增广后在增广路中后退的费用也为O(n)。所以在每一阶段中,修改增广路以及后退的复杂度为O(m*n)。
所以总复杂度就是O(m*n^2),由于一般情况下,边的数量要比点的数量大的多,所以时间复杂度是大大降低了的。
代码:
#include <iostream>#include <fstream>#include <stdio.h>#include <algorithm>#include <string.h>#include <iomanip>using namespace std;struct Tline{ int x,y,v; Tline(){v=0;}};const int MAXV = 10000006, MAXN = 206, MAXM = 406, INF = 2000000000;int n,m,a,b,v,Min,ans=0,fp=0;Tline connect[MAXM];int line[MAXN];int next[MAXM],head[MAXN],dis[MAXN];bool f[MAXN];void _add(int a,int b,int v){ connect[fp].x=a; connect[fp].y=b; connect[fp].v=v; next[fp]=head[a]; head[a]=fp; fp++;}bool BFS() //最短路分层 { int h=0,t=0; memset(f,0,sizeof f); line[t++]=1;f[1]=1; dis[1]=0; while (h<t) { for (int i=head[line[h]];i!=-1;i=next[i]) if (f[connect[i].y]==0 && connect[i].v>0) { f[connect[i].y]=1; dis[connect[i].y]=dis[line[h]]+1; //记录源点到这个点的最短距离 line[t++]=connect[i].y; } h++; } return f[n];}int DFS(int root,int Min){ int ans=0,tmp; if (root==n) {return Min;} for (int i=head[root];i!=-1;i=next[i]) { if (connect[i].v==0 || dis[connect[i].y]!=dis[root]+1) continue; //一定是从dis[root]层到dis[root]+1层 tmp=DFS(connect[i].y,min(Min-ans,connect[i].v)); connect[i].v-=tmp; connect[i^1].v+=tmp; ans+=tmp; if (ans==Min) return ans; //尽量多流 } return ans;}int main(){ while (~scanf("%d%d",&m,&n)) { memset(head,-1,sizeof head); memset(next,-1,sizeof next); fp=0;ans=0; for (int i=0;i<m;i++) { scanf("%d%d%d",&a,&b,&v); _add(a,b,v); //添加原有边 _add(b,a,0); //添加反向弧 } while (BFS()) { // for (int i=1;i<=n;i++) cout << dis[i] << ' ';cout << endl; Min=MAXV; ans+=DFS(1,INF); } printf("%d\n",ans); }}
- 2016.8.9最大流
- 最大流-最大利益
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- 最大流
- Spark 使用sortByKey进行二次排序
- 配置自己的VPN服务器ubuntu16
- 当前使用的IP地址有4个字节(32)组成,即IPV4编码方式。每个IP地址包换两部分:网络号和主机号。当分配给主机号的二进制位越多,则能标识的主机数就越多,相应地能标识的网络数就越少,反之亦然。
- 大数相加
- hdu 5832
- 2016.8.9最大流
- 关于GridView加载和滑动时造成的图片混乱问题总结
- [leetcode] 75. Sort Colors
- Trouble-Shooting:CRS未启动导致RAC无法启动的对应方法
- HDU OJ 5283 Senior's Fish
- 【Objective-C】栈(stack)和堆(heap)的区别
- Meet Android Studio
- STL算法(14)——for_each()
- Application Fundamentals