网络流算法——精选推荐

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

⽹络流算法
2018-03-13 19:02:13
在图论中,⽹络流(英语:Network flow)是指在⼀个每条边都有容量(capacity)的有向图分配流,使⼀条边的流量不会超过它的容量。

通常在运筹学中,有向图称为⽹络。

顶点称为节点(node)⽽边称为弧(arc)。

⼀道流必须匹配⼀个结点的进出的流量相同的限制,除⾮这是⼀个源点(source)──有较多向外的流,或是⼀个汇点(sink)──有较多向内的流。

⼀个⽹络可以⽤来模拟道路系统的交通量、管中的液体、电路中的电流或类似⼀些东西在⼀个结点的⽹络中游动的任何事物。

⼀、最⼤流最⼩割定理
最⼤流最⼩割定理提供了对于⼀个⽹络流,从源点到⽬标点的最⼤的流量等于最⼩割的每⼀条边的和。

这个定理说明,当⽹络达到最⼤流时,会有⼀个割集,这个割集中的所有边都达到饱和状态。

这等价于在⽹络中再也找不到⼀个从s到t的增⼴路径。

因为只要能找到⼀条增⼴路径,这条增⼴路径肯定要经过最⼩割集中的⼀条边,否则这个割集就不能称之为割集了。

既然这个割集中所有的边都饱和了,因此也就不会存在这样的增⼴路径了。

这个定理的意义在于给我们指明了⽅向:
任何算法,只要最后能达到“再也找不到⼀条增⼴路径”,就可以说明这个算法最后达到了最⼤流。

⼆、最⼤流问题
在优化理论中,最⼤流问题涉及到在⼀个单源点、单汇点的⽹络流中找到⼀条最⼤的流。

最⼤流问题可以被看作是⼀个更复杂的⽹络流问题(如循环问题(circulation problem))的特殊情况,。

s-t流(从源点s到汇点t)的最⼤值等于s-t割的最⼩容量,这被称为最⼤流最⼩割定理。

下⾯举例来说明这个问题:
问题描述:
给定⼀个有向图G=(V,E),把图中的边看作管道,每条边上有⼀个权值,表⽰该管道的流量上限。

给定源点s和汇点t,现在假设在s处有⼀个⽔源,t处有⼀个蓄⽔池,问从s到t的最⼤⽔流量是多少。

这个问题有如下的⼀些限制:
容量限制:也就是在每条通路上的流量都不能超过其capacity。

平衡条件:除了源点和汇点,其他的点的流⼊量需要等于流出量。

反对称:v到u的净流量等于u到v的净流量的相反。

问题求解:
⽅法⼀、朴素dfs
⼀种⾮常朴素的思路是,使⽤dfs每次寻找s->t的⼀条通路,然后将这条路上的流量值定义为其中最⼩的容量值,完成这次运输后,将通路上的所有边的容量值减去流量值,开始下⼀次的寻找,直到没有通路,完成算法。

这个算法看上去是没有什么问题的,因为每次都在增加流量直到不能增加为⽌,可是真的是这样么?
我们看下⾯的这个例⼦:
如果第⼀次dfs到的通路是s - > a - > b - > t,那么这次之后,⽹络中就再也没有从s - > t的通路了,按照算法的思路,这个⽹络的最⼤流就是100,然⽽,很明显的,这个⽹络的最⼤流是200。

因此,简单的使⽤dfs是不能很好的解决这个问题的,下⾯的Ford-Fulkerson算法解决了这个问题,⽽成为了⽹络流算法中的经典。

⽅法⼆、Ford-Fulkerson算法
在上⾯的例⼦中出错的原因是a - > b 的实际流量应该是0,但是我们过早的认为他们之间是有流量的,因此封锁了我们最⼤流继续增⼤的可能。

⼀个改进的思路:应能够修改已建⽴的流⽹络,使得“不合理”的流量被删掉。

⼀种实现:对上次dfs 时找到的流量路径上的边,添加⼀条“反向”边,反向边上的容量等于上次dfs 时找到的该边上的流量,然后再利⽤“反向”的容量和其他边上剩余的容量寻找路径。

下⾯我们重新对上⾯的例⼦进⾏分析:
1、第⼀次dfs依然是s - > a - > b - > t,与上次不同的是,这次我们会添加上反向边。

2、继续在新图上找可能通路,我们这次可以找到另⼀条流量为100的通路s - > b - > a - > t,从宏观上来看,反向边起到了“取消流”的功能,
也可以看成是两条通路的合并。

3、对第⼆次找到的路径添加反向边,我们发现图中已经没有从s - > t的通路了,于是该⽹络的最⼤流就是200。

添加反向边的证明:
构建残余⽹络时添加反向边a->b,容量是n,增⼴的时候发现了流量n-k,即新增了n-k的流量。

这n-k的流量,从a进,b出,最终流到汇。

现要证明这2n-k的从流量,在原图上确实是可以从源流到汇的。

在原图上可以如下分配流量,则能有2n-k从源流到汇点:
三、Ford-Fulkerson算法
Ford-Fulkerson算法:求最⼤流的过程,就是不断找到⼀条源到汇的路径,然后构建残余⽹络,再在残余⽹络上寻找新的路径,使总流量增加,然后形成新的残余⽹络,再寻找新路径….. 直到某个残余⽹络上找不到从源到汇的路径为⽌,最⼤流就算出来了。

残余⽹络 (Residual Network):在⼀个⽹络流图上,找到⼀条源到汇的路径(即找到了⼀个流量)后,对路径上所有的边,其容量都减去此次找到的流量,对路径上所有的边,都添加⼀条反向边,其容量也等于此次找到的流量,这样得到的新图,就称为原图的“残余⽹络”。

增⼴路径:每次寻找新流量并构造新残余⽹络的过程,就叫做寻找流量的“增⼴路径”,也叫“增⼴”。

算法时间复杂度分析:现在假设每条边的容量都是整数,这个算法每次都能将流⾄少增加1。

由于整个⽹络的流量最多不超过图中所有的边的容量和C,从⽽算法会结束现在来看复杂度找增⼴路径的算法可以⽤dfs,复杂度为O(V + E)。

dfs最多运⾏C次所以时间复杂度为C*(V +
E) =C * V ^ 2。

四、Edmonds-Karp算法
这个算法实现很简单但是注意到在图中C可能很⼤很⼤,⽐如说下⾯这张图,如果运⽓不好这种图会让你的程序执⾏200次dfs,虽然实际上最少只要2次我们就能得到最⼤流。

为了避免这个问题,我们每次在寻找增⼴路径的时候都按照bfs进⾏寻找,⽽不是按照dfs进⾏寻找,这就是Edmonds-Karp最短增⼴路算法。

时间复杂度:已经证明这种算法的复杂度上限为O(V*E^2)。

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
// POJ 1273
public class EdmondsKarp {
static int[][] G = new int[300][300];
static boolean[] visited = new boolean[300];
static int[] pre = new int[300];
static int v;
static int e;
static int augmentRoute() {
Arrays.fill(visited, false);
Arrays.fill(pre, 0);
Queue<Integer> queue = new LinkedList<Integer>(); queue.add(1);
boolean flag = false;
while (!queue.isEmpty()) {
int cur = queue.poll();
visited[cur] = true;
for (int i = 1; i <= v; i++) {
if (i == cur) continue;
if (G[cur][i] != 0 && !visited[i]) {
pre[i] = cur;
if (i == v) {
flag = true;
queue.clear();
break;
}
else queue.add(i);
}
}
}
if (!flag) return 0;
int minFlow = Integer.MAX_VALUE;
int tmp = v;
while (pre[tmp] != 0) {
minFlow = Math.min(minFlow, G[pre[tmp]][tmp]); tmp = pre[tmp];
}
tmp = v;
while (pre[tmp] != 0) {
G[pre[tmp]][tmp] -= minFlow;
G[tmp][pre[tmp]] += minFlow;
tmp = pre[tmp];
}
return minFlow;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int res = 0;
for (int i = 0; i < G.length; i++) {
Arrays.fill(G[i], 0);
}
e = sc.nextInt();
v = sc.nextInt();
int s, t, c;
for (int i = 0; i < e; i++) {
s = sc.nextInt();
t = sc.nextInt();
c = sc.nextInt();
G[s][t] += c;
}
int aug;
while ((aug = augmentRoute()) != 0) {
res += aug;
}
System.out.println(res);
}
}
}
五、Dinic算法
Edmonds-Karp已经是⼀种很不错的改进⽅案了,但是每次⽣成⼀条增⼴路径都需要对s到t调⽤BFS,如果能在⼀次增⼴的过程中,寻找到多条增⼴路径,就可以进⼀步提⾼算法的运⾏效率。

Dinic算法就是使⽤了BFS + DFS达到了以上的思路,完成了算法复杂度的进⼀步下降。

时间复杂度:O(V^2 * E)
算法流程:
1、使⽤BFS对残余⽹络进⾏分层,在分层时,只要进⾏到汇点的层次数被算出即可停⽌,因为按照该DFS的规则,和汇点同层或更下⼀层的节点,是不可能⾛到汇点的。

2、分完层后,从源点开始,⽤DFS从前⼀层向后⼀层反复寻找增⼴路(即要求DFS的每⼀步都必须要⾛到下⼀层的节点)。

3、DFS过程中,要是碰到了汇点,则说明找到了⼀条增⼴路径。

此时要增加总流量的值,消减路径上各边的容量,并添加反向边,即所谓的进⾏增⼴。

4、DFS找到⼀条增⼴路径后,并不⽴即结束,⽽是回溯后继续DFS寻找下⼀个增⼴路径。

回溯到的结点满⾜以下的条件:
1) DFS搜索树的树边(u,v)上的容量已经变成0。

即刚刚找到的增⼴路径上所增加的流量,等于(u,v)本次增⼴前的容量。

(DFS的过程中,是从u⾛到更下层的v的)
2) u是满⾜条件 1)的最上层的节点。

5、如果回溯到源点⽽且⽆法继续往下⾛了,DFS结束。

因此,⼀次DFS过程中,可以找到多条增⼴路径。

6、DFS结束后,对残余⽹络再次进⾏分层,然后再进⾏DFS当残余⽹络的分层操作⽆法算出汇点的层次(即BFS到达不了汇点)时,算法结束,最⼤流求出。

⼀般⽤栈实现DFS,这样就能从栈中提取出增⼴路径。

// POJ 1273
public class Dinic {
static int[][] G = new int[300][300];
static boolean[] visited = new boolean[300];
static int[] layer = new int[300];
static int v;
static int e;
static boolean countLayer() {
int depth = 0;
Arrays.fill(layer, -1);
layer[1] = 0;
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(1);
while (!queue.isEmpty()) {
int cur = queue.poll();
for (int i = 1; i <= v; i++) {
if (G[cur][i] > 0 && layer[i] == -1) {
layer[i] = layer[cur] + 1;
if (i == v) {
queue.clear();
return true;
}
queue.add(i);
}
}
}
return false;
}
static int dinic() {
int res = 0;
List<Integer> stack = new ArrayList<Integer>();
while (countLayer()) {
stack.add(1);
Arrays.fill(visited, false);
visited[1] = true;
while (!stack.isEmpty()) {
int cur = stack.get(stack.size() - 1);
if (cur == v) {
int minFlow = Integer.MAX_VALUE;
int minS = Integer.MAX_VALUE;
for (int i = 1; i < stack.size(); i++) {
int tmps = stack.get(i - 1);
int tmpe = stack.get(i);
if (minFlow > G[tmps][tmpe]) {
minFlow = G[tmps][tmpe];
minS = tmps;
}
}
// ⽣成残余⽹络
for (int i = 1; i < stack.size(); i++) {
int tmps = stack.get(i - 1);
int tmpe = stack.get(i);
G[tmps][tmpe] -= minFlow;
G[tmpe][tmps] += minFlow;
}
// 退栈到minS
while (!stack.isEmpty() && stack.get(stack.size() - 1) != minS) { stack.remove(stack.size() - 1);
}
res += minFlow;
}
else {
int i;
for (i = 1; i <= v; i++) {
if (G[cur][i] > 0 && layer[i] == layer[cur] + 1 && !visited[i]) { visited[i] = true;
stack.add(i);
break;
}
}
if (i > v) {
stack.remove(stack.size() - 1);
}
}
}
}
return res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
for (int i = 0; i < G.length; i++) {
Arrays.fill(G[i], 0);
}
e = sc.nextInt();
v = sc.nextInt();
int s, t, c;
for (int i = 0; i < e; i++) {
s = sc.nextInt();
t = sc.nextInt();
c = sc.nextInt();
G[s][t] += c;
}
System.out.println(dinic());
}
}
}
六、最⼩费⽤最⼤流
问题描述:
设有⼀个⽹络图 G(V,E) , V={s,a,b,c,…,s ’},E 中的每条边 (i,j) 对应⼀个容量 c(i,j) 与输送单位流量所需费⽤a(i,j) 。

如有⼀个运输⽅案(可⾏流),流量为 f(i, j) ,则最⼩费⽤最⼤流问题就是这样⼀个求极值问题:
其中 F 为 G 的最⼤流的集合,即在最⼤流中寻找⼀个费⽤最⼩的最⼤流。

算法流程:
反复⽤spfa算法做源到汇的最短路进⾏增⼴,边权值为边上单位费⽤。

反向边上的单位费⽤是负的。

直到⽆法增⼴,即为找到最⼩费⽤最⼤流。

成⽴原因:每次增⼴时,每增加1个流量,所增加的费⽤都是最⼩的。

相关文档
最新文档