[转载]最大流之Ford-Fulkerson方法详解及实现

转自:http://blog.csdn.net/ivan_zgj/article/details/51580993

最大流问题常常出现在物流配送中,可以规约为以下的图问题。最大流问题中,图中两个顶点之间不能同时存在一对相反方向的边。

image

边上的数字为该条边的容量,即在该条边上流过的量的上限值。最大流问题就是在满足容量限制条件下,使从起点s到终点t的流量达到最大。在介绍解决最大流问题的Ford-Fulkerson方法之前,先介绍一些基本概念。

1. 残存网络与增广路径

根据图和各条边上的流可以画出一幅图的残存网络如下所示。左图为流网络,右图为残存网络,其中流网络中边上的数字分别是流量和容量,如10/12,那么10为边上的流量,12为边的容量。残存网络中可能会存在一对相反方向的边,与流网络中相同的边代表的是流网络中该边的剩余容量,在流网络中不存在的边代表的则是其在流网络中反向边的已有流量,这部分流量可以通过“回流”减少。例如,右图残存网络中,边<s,v1>的剩余容量为4,其反向边<v1.s>的值为12,即左图流网络中的边<s,v1>的流量。在残存网络中,值为0的边不会画出,如边<v1,v2>。

image
image

残存网络描述了图中各边的剩余容量以及可以通过“回流”删除的流量大小。在Ford-Fulkerson方法中,正是通过在残存网络中寻找一条从s到t的增广路径,并对应这条路径上的各边对流网络中的各边的流进行修改。如果路径上的一条边存在于流网络中,那么对该边的流增加,否则对其反向边的流减少。增加或减少的值是确定的,就是该增广路径上值最小的边。

2. Ford-Fulkerson方法

Ford-Fulkerson方法的正确性依赖于这个定理:当残存网络中不存在一条从s到t的增广路径,那么该图已经达到最大流。这个定理的证明及一些与其等同的定理可以参考《算法导论》。

Ford-Fulkerson方法的伪代码如下。其中<u,v>代表顶点u到顶点v的一条边,<u,v>.f表示该边的流量,c是边容量矩阵,c(i,j)表示边<i,j>的容量,当边<i,j>不存在时,c(i,j)=0。e为残存网络矩阵,e(i,j)表示边<i,j>的值,当边<i,j>不存在时,e(i,j)=0。E表示边的集合。f表示流网络。

Ford-Fulkerson

    for <u,v> ∈ E

        <u,v>.f = 0

    while find a route from s to t in e

        m = min(<u,v>.f, <u,v>  ∈ route)

        for <u,v> ∈ route

            if <u,v>  ∈ f

                <u,v>.f = <u,v>.f + m

            else

                <v,u>.f = <v,u>.f - m

Ford-Fulkerson方法首先对图中的所有边的流量初始化为零值,然后开始进入循环:如果在残存网络中可以找到一条从s到t的增广路径,那么要找到这条这条路径上值最小的边,然后根据该值来更新流网络。

Ford-Fulkerson有很多种实现,主要不同点在于如何寻找增广路径。最开始提出该方法的Ford和Fulkerson同学在其论文中都是使用广度优先搜索实现的,其时间复杂度为O(VE),整个算法的时间复杂度为O(VE^2)。

下面我给出一个应用Bellman-Ford计算单源最短路径的算法实现寻找一条增广路径,对于用邻接矩阵表示的图来说,该实现的时间复杂度为O(V^3),对于用邻接表表示的图来说,时间复杂度则为O(VE)。

// 寻找增广路径
int findRoute(int **e, int vertexNum, int *priorMatrix, int s,int t)
{
    s--; t--;
    int *d = (int *)malloc(sizeof(int)*vertexNum);
    // initialize
    for (int i = 0; i < vertexNum; i++)
    {
        d[i] = 0;
        priorMatrix[i] = -1;
    }
    d[s] = 1;
    // 反复用边<i,j>做松弛操作,将<s,...,j>更新为<s,...,i,j>
    for (int k = 0; k < vertexNum; k++)
    {
        for (int i = 0; i < vertexNum; i++)
        {
            for (int j = 0; j < vertexNum; j++)
            {
                if (d[j] == 0)
                {
                    d[j] |= (d[i] & (*((int*)e + i*vertexNum + j) > 0));
                    if (d[j] == 1)
                    {
                        priorMatrix[j] = i;
                    }
                }
            }
        }
    }
    if (d[t] == 0)  return 0;

    int min = INT_MAX;
    int pre = priorMatrix[t];
    while (pre != -1)
    {
        if (min > *((int*)e + pre*vertexNum + t))
        {
            min = *((int*)e + pre*vertexNum + t);
        }
        t = pre;
        pre = priorMatrix[t];
    }
    return min;
}

该实现应用了计算图的最短路径方法中的思想,对图中的边反复在松弛操作,从而计算得到一个源点到其它所有点的路径。这里我们不需要计算最短路径,只要找到一条可行路径即可。上述findRoute方法的实现原理可以参考我前面的一篇文章 单源最短路径之Bellman-Ford算法 。在寻找路径的同时,我们还要记录一个前驱子图priorMatrix,其本质上是一个一位数组,其记录了从顶点s到其它顶点的一条可行路径上的终点的前一个顶点。于是我们就可以从前驱子图中找到从s到t的一条完整路径。其正确性由图的最短路径的计算方法思想保证。具体可以参考我另一篇博客 结点对最短路径之Floyd算法详解及实现

下面给出根据图和流网络计算残存网络的代码。

// 计算残存网络
void calculateENet(int **c, int vertexNum, int **f, int **e)
{
    for (int i = 0; i < vertexNum; i++)
    {
        for (int j = 0; j < vertexNum; j++)
        {
            int a = *((int*)c + i*vertexNum + j);
            if (a != 0)
            {
                *((int*)e + i*vertexNum + j) = a - *((int*)f + i*vertexNum + j);
                *((int*)e + j*vertexNum + i) = *((int*)f + i*vertexNum + j);
            }
            else
            {
                *((int*)e + i*vertexNum + j) = 0;
            }
        }
    }
}

下面给出Ford-Fulkerson方法的实现代码。

/**
* Ford-Fulkerson方法的一种实现
* @param c 二维矩阵,记录每条边的容量
* @param vertexNum 顶点个数,包括起点和终点
* @param s 起点编号,编号从1开始
* @param t 终点编号
* @param f 输出流网络矩阵,二维矩阵,记录每条边的流量
*/
void Ford_Fulkerson(int **c, int vertexNum, int s, int t, int **f)
{
    int *e = (int *)malloc(sizeof(int)*vertexNum*vertexNum);    // 残存网络
    int *priorMatrix = (int *)malloc(sizeof(int)*vertexNum);    // 增广路径的前驱子图

    // initialize
    for (int i = 0; i < vertexNum;i++)
    {
        for (int j = 0; j < vertexNum; j++)
        {
            *(f + i*vertexNum + j) = 0;
        }
    }

    while (1)
    {
        calculateENet(c, vertexNum, (int **)f, (int **)e);  // 计算残存网络
        int min;
        if ((min = findRoute((int **)e, vertexNum, priorMatrix, s, t)) == 0)    // 寻找增广路径及其最小流值
        {
            break;
        }
        int pre = priorMatrix[t - 1];
        int next = t - 1;
        while (pre != -1)       // 按增广路径更新流网络
        {
            if (*((int*)c + pre * vertexNum + next) != 0)
            {
                *((int*)f + pre * vertexNum + next) += min;
            }
            else
            {
                *((int*)f + next * vertexNum + pre) -= min;
            }
            next = pre;
            pre = priorMatrix[pre];
        }
    }
}

3. 测试及效果

下面给出用于测试的代码。

void testFord()
{
    int c[6][6] = { 0,      16,     13,     0,      0,      0,
                    0,      0,      0,      12,     0,      0,
                    0,      4,      0,      0,      14,     0,
                    0,      0,      9,      0,      0,      20,
                    0,      0,      0,      7,      0,      4,
                    0,      0,      0,      0,      0,      0   };
    int f[6][6];
    Ford_Fulkerson((int **)c, 6, 1, 6, (int **)f);
    for (int i = 0; i < 6; i++)
    {
        for (int j = 0; j < 6; j++)
        {
            int flow = f[i][j];
            if (flow != 0)
            {
                printf("%d -> %d : %d\n", i + 1, j + 1, flow);
            }
        }
    }
}

上述代码构造的图如下所示。

image

运行结果如下,其中1为顶点s,5为顶点t,2~5依次为顶点v1、v2、v3和v4。

image

流网络和残存网络如下所示,其中左图为流网络,右图为残存网络。

image

我们可以看到残存网络中的确已经不存在一条从s到t的路径了。此时Ford-Fulkerson方法的循环应该终止,最大流量为各边的流量相加之和,即76。

完整的程序可以看到我的github项目 数据结构与算法

这个项目里面有本博客介绍过的和没有介绍的以及将要介绍的《算法导论》中部分主要的数据结构和算法的C实现,有兴趣的可以fork或者star一下哦~ 由于本人还在研究《算法导论》,所以这个项目还会持续更新哦~ 大家一起好好学习~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 目录 1.流网络及最大流问题1.1 流网络1.2 最大流问题1.3 最大流问题的三种解法对比 2.Ford-Ful...
    王侦阅读 4,458评论 0 3
  • 最大流&&最小费用最大流&&最大二分匹配 中文是2017年8月的笔记,英文是2018.11月的笔记 英文笔记来自于...
    廖少少阅读 34,064评论 6 20
  • 本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致...
    Andrew_liu阅读 36,806评论 14 31
  • 现实生活中有很大一类问题可以用简洁明了的图论语言来描述,可以转化为图论问题。 相关定义 图可以表示为G=(V, E...
    芥丶未央阅读 1,560评论 0 7
  • 一、【考试前一天】 1、最好去考场熟悉一下路线 2、早点睡,不要熬夜看书,保证充足睡眠。 3.查对考试必带品:a、...
    小超超1213阅读 252评论 0 0