网络最大流算法

来源:互联网 发布:淘宝人生起点 编辑:程序博客网 时间:2024/05/14 02:34

最大流也是图的一种常用的算法,许多系统包含了流量问题。例如交通系统有车流量,金融系统有现金流,控制系统有信息流等。许多流问题主要是确定这类系统网络所能承受的最大流量以及如何达到这个最大流量。 如果你想比较彻底的弄清楚网络最大流的原理, 你需要了解三个关键的概念:残留网络,增广路径,最大流最小割定。 这三个概念在算法

导论上有较详细的说明再次就不再讲述了,估计也没有算法导论上讲的清楚。所以可以先翻阅一下<<算法导论>>。下面我就讲述一下一个方法和两个算法。


1.Ford-Fulkerson 方法

Ford-Fulkerson是一种方法,它只提供思想,因此并不是一个算法。所有增广路算法的基础都是 Ford - Fulkerson 方法的。给定一个有向网络 G(V,E) 以及源点 s 终点 t ,FF 方法描述如下:
Ford-Fulkerson 方法 (G,s,t)
1 将各边上流量 f 初始化为 0
2 while 存在一条增广路径 p
3     do 沿路径 p 增广流量 f
4 return f
假设有向网络 G 中边 (i,j) 的容量为 c(i,j) ,当前流量为 f(i,j) ,则此边的剩余流量即为 r(i,j) = c(i,j) - f(i,j) ,其反向边的剩余流量为(j,i) = f(i,j),有向网中所有剩余流量 r(i,j) > 0 的边构成残量网络 Gf。增广路径是在参与网络中寻找的。通过反复宣召增广路径,直至增广路径都被找出来,根据最大流最小割定理,当不包含增广路径时,f是G中的一个最大流。因此求出增广路径的效率决定着算法的效率。不同的方法对增广路径的求法有所不同, 因此它们的效率也因此不同。接下来用以个实际的例子先看一下算法的基本流程。


Edmonds - Karp 算法



首先来了解一下 Shortest Augmenting Path (SAP), 它 是每次寻找最短增广路的一类算法,Edmonds - Karp 算法以及后来著名的 Dinic 算法都属于此。SAP 类算法可统一描述如下:
Shortest Augmenting Path
1 x <-- 0
2 while 在残量网络 Gx 中存在增广路 s ~> t
3     do 找一条最短的增广路径 P
4        delta <-- min{rij:(i,j) 属于 P}
5        沿 P 增广 delta 大小的流量
6        更新残量网络 Gx
7 return x
EK算法的核心思想就是,就是反复寻找从s到t的增广路径,然后找出增广路径上每条边上(容量-流量)delta的最小值, 因为用min(delta)大的流量流过该增广路径总是安全可行的。在寻找增广路径上面EK算法采用的是BFS策略。每次修改残留网络。下面给出EK算法的代码:
  1. //代码是POJ 1273
    #include <queue>
    #include <string>
    #include <string.h>
    #include <stdio.h>
    #include <iostream>
    using namespace std;
    const int MAXN = 300;
    int N, M;
    int c[MAXN][MAXN];
    bool visit[MAXN];
    int pre[MAXN];
    bool BFS(int start, int end)
    {
      memset(pre, -1, sizeof(pre));
      memset(visit, false, sizeof(visit));
      int now;
      queue<int> q;
      q.push(start);
      visit[start] = true;
      while(!q.empty())
      {
        now = q.front();
        q.pop();
        if(now == end)
          return true;
        for(size_t i = 1; i <= M; ++i)
        {
          if(c[now][i] && !visit[i])
          {
            pre[i] = now;
            visit[i] = true;
            q.push(i);
          }
        }
      }
      return false;
    }
    int EK(int start, int end)
    {
      int res = 0;
      while(1)
      {
        if(!BFS(start, end))//寻找增广路径
        {
          break;
        }
        int min = 1<<30;
        for(int i = end; i != 1; i = pre[i])
        {
          if(min > c[pre[i]][i])
            min = c[pre[i]][i];
        }
        for(int i = end; i != 1; i = pre[i])//根据增广路径修改残留网络
        {
          c[pre[i]][i] -= min;
          c[i][pre[i]] += min;
        }
        res += min;
      }
      return res;
    }


    int main()
    {
      while(cin >> N >> M)
      {
        int ans = 0;
        int s, e, cap;
        memset(c, 0, sizeof(c));
        for(size_t i = 1; i <= N; ++i)
        {
          cin >> s >> e >> cap;
          c[s][e] += cap;
        }
        ans = EK(1, M);
        cout << ans << endl;
      }
    }


E-K 算法的时间复杂度为 O(VE^2),因为它要搜索距离小于最短路径的所有分支后才能找到终点,而且要进行频繁的BFS过程,导致效率比较低。因此在实际应用中不会成为我们的首选。接下来的Dinic是在它的基础上进行了优化效率大大提高。

Dinic算法 

由于BFS寻找最短路径较慢, 而DFS不能保证宣召到最短路径所以 Dinic提出了一种结合了 BFS 与 DFS 的优势,采用构造分层网络的方法可以较快找到最短增广路,此算法又称为阻塞流算法(Blocking Flow Algorithm) 
Dinic算法的基本思路:
  1. 根据残量网络计算层次图。
  2. 在层次图中使用DFS进行增广直到不存在增广路
  3. 重复以上步骤直到无法增广
首先定义分层网络 AN(f)。在残量网络中从源点 s 起始进行 BFS,这样每个顶点在 BFS 树中会得到一个距源点 s 的距离 d,如 d(s) = 0,直接从 s 出发可到达的点距离为 1,下一层距离为2 ... 。称所有具有相同距离的顶点位于同一层,在分层网络中,只保留满足条件 d(i) + 1 = d(j) 的边,这样在分层网络中的任意路径就成为到达此顶点的最短路径.

注意:

一般情况下在Dinic算法中,我们只记录某一边的剩余流量.

  • 残留网络:包含反向弧的有向图,Dinic要循环的,每次修改过的图都是残量网络,
  • 层次图:分层图,以[从原点到某点的最短距离]分层的图,距离相等的为一层
  • DFS:寻找增广路径。
  • 增广  :在现有流量基础上发现新的路径,扩大发现的最大流量(注意:增加量不一定是这条路径的流量,而是新的流量与上次流量之差)
  • 增广路:在现有流量基础上发现的新路径
  • 剩余流量:当一条边被增广之后(即它是增广路的一部分,或者说增广路通过这条边),这条边还能通过的流量.
  • 反向弧:我们在Dinic算法中,对于一条有向边,我们需要建立另一条反向边(弧),当正向(输入数据)边剩余流量减少I时,反向弧剩余流量增加I

Dinic 算法每次用一遍 BFS 构建分层网络 AN(f),然后在 AN(f) 中一遍 DFS 找到所有到终点 t 的路径增广;之后重新构造 AN(f),若终点 t 不在 AN(f) 中则算法结束。DFS 部分算法可描述如下:

 1 p <-- s
 2 while s 的出度 > 0 do
 3     u <-- p.top
 4     if u != t then
 5         if u 的出度 > 0 then
 6             设 (u,v) 为 AN(f) 中一条边
 7             p <-- p, v
 8         else
 9             从 p 和 AN(f) 中删除点 u 以及和 u 连接的所有边
10     else
11         沿 p 增广
12         令 p.top 为从 s 沿 p 可到达的最后顶点
13 end while

实现代码:
  1. #include <queue>
  2. #include <iostream>
    #include <string.h>
    #include <cstdlib>
    using namespace std;

    const int MAX = 300;
    const int INF = 1 << 30;
    int cap[MAX][MAX];
    int level[MAX];
    int M, N;
    bool BFS(int start, int end)
    {
      memset(level, 0, sizeof(level));
      queue<int> q;
      q.push(start);
      level[start] = 1;
      while(!q.empty())
      {
        int now = q.front();
        q.pop();
        for(size_t i = 1; i <= M; ++i)
        {
          if(cap[now][i] > 0 && !level[i])
          {
            level[i] = level[now] + 1;
            q.push(i);
          }
        }
      }
      return level[end] != 0;
    }

    int DFS(int start, int flow = INF)
    {
      if(start == M)
        return flow;
      int t;
      for(size_t i = 1; i <= M; ++i)
      {
        if(level[i] == level[start] + 1 && cap[start][i] > 0 
           && (t = DFS(i, min(flow, cap[start][i]))) )
        {
          cap[start][i] -= t;
          cap[i][start] += t;
          return t;
        }
      }
      return 0;
    }


    int Dinic(int start, int end)
    {
      int ans = 0;


      while(BFS(start, end))
      {
        cnt++;
        ans += DFS(start);
      }
      return ans;
    }


    int main()
    {
      while(cin >> N >> M )
      {
        int ans;
        int s, t, c;
        memset(cap, 0, sizeof(cap));
        for(size_t i = 0; i < N ; ++i)
        {
          cin >> s >> t >> c;
          cap[s][t] += c;
        }
        cout << Dinic(1, M) << endl;
      }
    }











原创粉丝点击