Edmond-Karp 最大流算法详解

本文详细介绍了 Edmond-Karp 算法,作为 Ford-Fulkerson 方法的一种优化,用于解决最大流问题。通过分析 Ford-Fulkerson 算法存在的问题,即深度优先搜索可能导致的高时间复杂度,提出了使用广度优先搜索(BFS)寻找增广路径来改善效率。文章阐述了 BFS 在寻找增广路径中的优势,并给出了 EK 算法的实现过程、时间复杂度和适用情况。最后,指出 EK 算法适用于边数量较少的稀疏图,而 FF 算法则适合边数量较多的稠密图。
摘要由CSDN通过智能技术生成

知识梳理

  • 「初识最大流问题」中,我们了解了什么是流网络模型、什么是最大流问题、以及在流网络中 的增广路(Augmenting Path)概念

  • 「Ford-Fulkerson 最大流求解方法」中,我们学习了 Ford-Fulkerson 的最大流问题求解方法和思路:不断的深度优先搜索,直到没有增广路为止则获得最大流

  • 「二分匹配的最大流思维」中,通过增加超级源和超级汇来修改二分图,从而将二分匹配问题转换成了最大流问题,最后通过 Ford-Fulkerson 方法解决。

以上三篇前导文章都是在认识和使用最大流这种问题模型,从而进行一些算法思考。但是我们始终没有关心 Ford-Fulkerson 方法的时间复杂度问题。

这篇文章会讲述一个求解最大流问题的 EK 算法,从而优化在某些场景下最大流问题的求解效率

Ford-Fulkerson 方法有什么问题?

我们知道,在之前讨论的图中,根据 Ford-Fulkerson 方法,我们采用深度优先搜索(下文简称 DFS),不断的去寻找查询增广路,从而增加超级汇点的流量。先来复习一下 Ford-Fulkerson 方法的算法流程:

  1. 使用 DFS 搜索 出一条增广路;

  2. 在这条路径中所有的边的容量减去这条增广路的流量 f,并建立容量为 f 的反向边;

  3. 返回操作一,直到没有增广路;

在这个算法流程中,为将 “使用 DFS” 进行了加粗,你一定察觉到一些端倪。我们来从这个角度来思考一下:

DFS 访问了不必要的增广路

假设有一个网络流如上图所示,我们可以一眼看出最大流是 99。但是在我们代码中使用 Fold-Fulkerson 算法进行查找增广路的过程中,由于根据标号进行搜索,所以一定会先找到 S → A → C → .... → D → E → T 这条增广路。于是我们就浪费了很多开销。

其实我们在这个问题中,只要找到 S → B → E → T 这条增广路,就可以将 T 的入度达到满流状态,后续也就直接结束了。

算法导论上给出的最坏情况分析

如果有一个图,某一条边是一个“噪声边”(所谓“噪声”就是指它在最终的结果中是没有对汇点进行增广的,也就是没有贡献流量的),它的容量很少,并且它在 DFS 搜索中,位置十分靠前,每一次都优先搜到了这一条增广路,那么在每一个二次搜索增广路的时候,都会去抵消它的流量,通过反向边完成一次真实的增广操作。 这样问题就十分严重了。我举一个例子:

Ford-Fulkerson 方法的最差情况

上面这个图,我们看一眼就知道它的结果是 s → 0 → t 和 s → 1 → t 这两个增广路贡献的流量和 199,但是由于 0 → 1 这条边的序号十分靠前,所以每次在进行搜索增广路的过程中,就会优先使用 S → A → B → T 这条边;然后在第二次选择增广路时,我们选择了 S → B → A → T ,如此这样反复,我们发现每一次找到增广路,只增加了 1 个单位的流量,所以如此反复 199 次才能完成最大流算法的计算。

我们用动图来描述一下这个场景:

结合代码,我们来看看到底进行了多少次的增广操作:

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;

#define INF 0x3f3f3f3f

const int MAX_V = 1e4 + 5;

struct Edge {
    int to, cap, rev;
    Edge(int _to, int _cap, int _rev): to(_to), cap(_cap), rev(_rev) {}
};

// 邻接表记录点和其相连的边
// 比如节点 1 的所有出度边集 G[1]
vector<Edge> G[MAX_V]; 

void add_edge(int from, int to, int cap) {
    G[from].push_back(Edge(to, cap, G[to].size()));
    G[to].push_back(Edge(from, 0, G[from].size() - 1));
}

bool used[MAX_V];

int dfs(int c, int t, int f) {
    if (c == t) {
        return f;
    }
    used[c] = true;
    for (int i = 0; i < G[c].size(); ++ i) {
        Edge &e = G[c][i];
        if (!used[e.to] && e.cap > 0) {
            int d = dfs(e.to, t, min(f, e.cap));
            if (d > 0) {
                e.cap -= d;
                G[e.to][e.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}

int max_flow(int s, int t) {
    int flow = 0;
    int cnt = 0;
    for (;;) {
        memset(used, 0, sizeof(used));
        int f = dfs(s, t, INF);
        cnt += 1;
        if (f == 0) {
            cout << cnt << endl; // 5 - 也就是说只进行了 5 次增广路查询
            return flow;
        }
        flow += f;
    }
}

int main() {
  // 0: S点
    // 1: A点
    // 2: B点
    // 3: T点
    add_edge(0, 1, 99);
    add_edge(0, 2, 100);
    add_edge(1, 2, 1);
    add_edge(1, 3, 100);
    add_edge(2, 3, 100);
    int ret = max_flow(0, 3);

    cout << ret << endl; // 199 
}

但我们发现,即使先搜索了 S → A → B → T 这条增广路,也不会出现这种最坏情况。原因是因为我们所实现的 Ford-Fulkerson 方法是使用 DFS 深度优先搜索查找增广路,在实现中有这句:

int d = dfs(e.to, t, min(f, e.cap));

这个 DFS 中会有回溯流程,也就是说,当我们找到 S → A → B → T 之后,对所有边的容量减 1 ,此时回溯到了 A 点,则又会继续查找 S → A → T 这条增广路。所以我们并不能看到《算法导论》中讨论的这种最差情况。

虽然这种极限情况是无法得到的,但是我们也知道了 传统的 Ford-Fulkerson 方法还是存在优化的可能

基于 DFS 的 FF 方法复杂度分析

从上面这里例子,你已经发现了,当基于 DFS 的 FF 算法的最差情况复杂度是和最大流相关的。假设我们有 E 条边并且最大流是 F,每次 DFS 查增广路则需要 O(E) 的复杂度,当最大流是 E 的时候,我们要进行最多 E 次的增广路查找。

所以基于 DFS 的 FF 算法的时间复杂度是:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值