Menu
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE
     ❯   

DSA Edmonds-Karp 算法

Edmonds-Karp 算法解决最大流问题。

寻找最大流在许多领域都有帮助:用于优化网络流量、制造、供应链和物流,或航空公司调度。

Edmonds-Karp 算法

Edmonds-Karp 算法解决 最大流问题 对于有向图。

流从源顶点 (\(s\)) 开始,最终到达汇点顶点 (\(t\)),图中的每条边都允许流量,流量受容量限制。

Edmonds-Karp 算法与 Ford-Fulkerson 算法 非常相似,区别在于 Edmonds-Karp 算法使用 广度优先搜索 (BFS) 来找到增加流量的增广路径。

{{edge.flow}}/{{edge.capacity}} {{vertex.name}}

最大流:{{maxFlow}}

{{statusText}}

Edmonds-Karp 算法的工作原理是使用广度优先搜索 (BFS) 来找到一条从源点到汇点(称为增广路径)的具有可用容量的路径,然后通过该路径发送尽可能多的流量。

Edmonds-Karp 算法继续寻找新的路径,直到达到最大流为止。

在上面的模拟中,Edmonds-Karp 算法解决了最大流问题:它找出从源顶点 \(s\) 到汇点顶点 \(t\) 可以发送多少流量,最大流量为 8。

上面模拟中的数字以分数形式写出,其中第一个数字是流量,第二个数字是容量(该边上的最大可能流量)。例如,0/7 在边 \(s \rightarrow v_2\) 上,表示在该边上流量为 0,容量为 7

您可以看到下面关于 Edmonds-Karp 算法工作原理的基本逐步说明,但我们稍后需要更详细地说明才能真正理解它。

工作原理

  1. 从所有边的流量为零开始。
  2. 使用 BFS 找到一条可以增加流量的增广路径
  3. 执行瓶颈计算,找出可以通过该增广路径发送多少流量。
  4. 增加在增广路径中的每条边上发现的瓶颈计算值找到的流量。
  5. 重复步骤 2-4,直到找到最大流。当无法再找到新的增广路径时,就会发生这种情况。

Edmonds-Karp 中的残余网络

Edmonds-Karp 算法的工作原理是创建和使用一个称为残余网络的东西,它代表原始图。

在残余网络中,每条边都具有残余容量,它是该边的原始容量减去该边上的流量。残余容量可以被视为具有某些流量的边的剩余容量。

例如,如果 \( v_3 \rightarrow v_4 \) 边上的流量为 2,容量为 3,则该边上的残余流量为 1,因为该边还可以发送 1 个单位的流量。


Edmonds-Karp 中的反向边

Edmonds-Karp 算法还使用称为反向边的东西来发送回流量。这对于增加总流量很有用。

为了发送回流量,在网络中每条原始边的相反方向创建一条反向边。Edmonds-Karp 算法然后可以使用这些反向边在相反方向发送流量。

反向边没有流量或容量,只有残余容量。反向边的残余容量始终与相应原始边上的流量相同。

在我们的示例中,边 \( v_1 \rightarrow v_3 \) 的流量为 2,这意味着对应反向边 \( v_3 \rightarrow v_1 \) 上的残余容量为 2。

这仅仅意味着当原始边 \( v_1 \rightarrow v_3 \) 上的流量为 2 时,就有可能在该边上以相反方向发送相同数量的流量。使用反向边推送回流量也可以看作是撤销已创建流量的一部分。

具有边上的残余容量的残余网络的概念,以及反向边的概念,是 Edmonds-Karp 算法工作原理的核心,我们将在本页面的下方进一步实现该算法时更详细地说明这一点。


手动运行

图中最初没有流量。

Edmonds-Karp 算法首先使用广度优先搜索找到一条可以增加流量的增广路径,即 \(s \rightarrow v_1 \rightarrow v_3 \rightarrow t\)。

找到增广路径后,会执行瓶颈计算以找出可以通过该路径发送多少流量,该流量为:2。

因此,在增广路径中的每条边上发送了 2 个单位的流量。

{{edge.flow}}/{{edge.capacity}} {{vertex.name}}

Edmonds-Karp 算法的下一次迭代是再次执行这些步骤:找到一条新的增广路径,找出该路径上的流量可以增加多少,并相应地增加该路径上的边的流量。

发现下一条增广路径是 \(s \rightarrow v_1 \rightarrow v_4 \rightarrow t \)。

该路径上的流量只能增加 1,因为 \( s \rightarrow v_1 \) 边上只有一格空间可以容纳一个单位的流量。

{{edge.flow}}/{{edge.capacity}} {{vertex.name}}

发现下一条增广路径是 \(s \rightarrow v_2 \rightarrow v_4 \rightarrow t\)。

该路径上的流量可以增加 3。瓶颈(限制边)是 \( v_2 \rightarrow v_4 \),因为容量为 3。

{{edge.flow}}/{{edge.capacity}} {{vertex.name}}

发现最后一条增广路径是 \(s \rightarrow v_2 \rightarrow v_1 \rightarrow v_4 \rightarrow t\)。

该路径上的流量只能增加 2,因为边 \( v_4 \rightarrow t \) 是该路径上的瓶颈,只有 2 个单位的空间可以容纳更多流量 (\(capacity-flow=1\))。

{{edge.flow}}/{{edge.capacity}} {{vertex.name}}

此时,无法找到新的增广路径(无法找到一条可以从 \(s\) 到 \(t\) 发送更多流量的路径),这意味着已经找到了最大流,Edmonds-Karp 算法完成。

最大流为 8。如上图所示,流出源顶点 \(s\) 的流量 (8) 与流入汇点顶点 \(t\) 的流量相同。

此外,如果您选择除 \(s\) 或 \(t\) 之外的任何其他顶点,您会发现流入该顶点的流量与流出该顶点的流量相同。这就是我们所说的流量守恒,对于所有此类流量网络(每条边都有流量和容量的有向图)都必须满足该条件。


Edmonds-Karp 算法的实现

为了实现 Edmonds-Karp 算法,我们创建了一个 Graph 类。

The Graph 代表图及其顶点和边

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size

    def add_edge(self, u, v, c):
        self.adj_matrix[u][v] = c

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

第 3 行:我们创建了 adj_matrix 来保存所有边和边容量。初始值设置为 0

第 4 行:size 是图中的顶点数。

第 5 行:vertex_data 保存所有顶点的名称。

第 7-8 行:add_edge 方法用于从顶点 u 添加一条边到顶点 v,容量为 c

第 10-12 行:add_vertex_data 方法用于将顶点名称添加到图中。顶点的索引由 vertex 参数给出,data 是顶点的名称。

The Graph 类还包含 bfs 方法,使用广度优先搜索来查找增广路径

    def bfs(self, s, t, parent):
        visited = [False] * self.size
        queue = []  # Using list as a queue
        queue.append(s)
        visited[s] = True

        while queue:
            u = queue.pop(0)  # Pop from the start of the list

            for ind, val in enumerate(self.adj_matrix[u]):
                if not visited[ind] and val > 0:
                    queue.append(ind)
                    visited[ind] = True
                    parent[ind] = u

        return visited[t]

第 15-18 行:visited 数组有助于在搜索增广路径时避免重复访问相同的顶点。The queue 保存要探索的顶点,搜索始终从源顶点 s 开始。

第 20-21 行:只要 queue 中还有待探索的顶点,就从 queue 中取出第一个顶点,以便找到从该顶点到下一个顶点的路径。

第 23 行:对于当前顶点的每个相邻顶点。

第 24-27 行:如果相邻顶点尚未访问,并且到该顶点的边存在剩余容量:将它添加到需要探索的顶点队列中,将其标记为已访问,并将相邻顶点的 parent 设置为当前顶点 u

parent 数组保存顶点的父节点,从而从汇点向后创建一条通往源点的路径。 parent 随后在 Edmonds-Karp 算法中使用,位于 bfs 方法之外,用于在增广路径中增加流量。

第 29 行:最后一行返回 visited[t],如果增广路径以汇点 t 结束,则该值为 true。 返回 true 表示找到了增广路径。

edmonds_karp 方法是我们添加到 Graph 类中的最后一个方法。

    def edmonds_karp(self, source, sink):
        parent = [-1] * self.size
        max_flow = 0

        while self.bfs(source, sink, parent):
            path_flow = float("Inf")
            s = sink
            while(s != source):
                path_flow = min(path_flow, self.adj_matrix[parent[s]][s])
                s = parent[s]

            max_flow += path_flow
            v = sink
            while(v != source):
                u = parent[v]
                self.adj_matrix[u][v] -= path_flow
                self.adj_matrix[v][u] += path_flow
                v = parent[v]

            path = []
            v = sink
            while(v != source):
                path.append(v)
                v = parent[v]
            path.append(source)
            path.reverse()
            path_names = [self.vertex_data[node] for node in path]
            print("Path:", " -> ".join(path_names), ", Flow:", path_flow)

        return max_flow

最初, parent 数组保存无效索引值,因为一开始没有增广路径, max_flow0,并且 while 循环只要存在增广路径以增加流量,就会不断增加 max_flow

第 35 行:外部 while 循环确保 Edmonds-Karp 算法只要存在增广路径以增加流量,就不断增加流量。

第 36-37 行:增广路径上的初始流量为无穷大,将从汇点开始计算可能的流量增加。

第 38-40 行: path_flow 的值通过从汇点向源点反向遍历找到。路径上最小剩余容量决定了路径上可以发送多少流量。

第 42 行: path_flow 增加 path_flow

第 44-48 行:逐步遍历增广路径,从汇点向源点反向遍历,正向边的剩余容量减少 path_flow,反向边的剩余容量增加 path_flow

第 50-58 行:代码的这一部分只是用于打印,以便我们能够跟踪每次找到增广路径时,通过该路径发送了多少流量。

定义完 Graph 类后,必须定义顶点和边来初始化特定图,Edmonds-Karp 算法示例的完整代码如下所示

示例

Python

class Graph:
    def __init__(self, size):
        self.adj_matrix = [[0] * size for _ in range(size)]
        self.size = size
        self.vertex_data = [''] * size

    def add_edge(self, u, v, c):
        self.adj_matrix[u][v] = c

    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

    def bfs(self, s, t, parent):
        visited = [False] * self.size
        queue = []  # Using list as a queue
        queue.append(s)
        visited[s] = True

        while queue:
            u = queue.pop(0)  # Pop from the start of the list

            for ind, val in enumerate(self.adj_matrix[u]):
                if not visited[ind] and val > 0:
                    queue.append(ind)
                    visited[ind] = True
                    parent[ind] = u

        return visited[t]

    def edmonds_karp(self, source, sink):
        parent = [-1] * self.size
        max_flow = 0

        while self.bfs(source, sink, parent):
            path_flow = float("Inf")
            s = sink
            while(s != source):
                path_flow = min(path_flow, self.adj_matrix[parent[s]][s])
                s = parent[s]

            max_flow += path_flow
            v = sink
            while(v != source):
                u = parent[v]
                self.adj_matrix[u][v] -= path_flow
                self.adj_matrix[v][u] += path_flow
                v = parent[v]

            path = []
            v = sink
            while(v != source):
                path.append(v)
                v = parent[v]
            path.append(source)
            path.reverse()
            path_names = [self.vertex_data[node] for node in path]
            print("Path:", " -> ".join(path_names), ", Flow:", path_flow)

        return max_flow

# Example usage:
g = Graph(6)
vertex_names = ['s', 'v1', 'v2', 'v3', 'v4', 't']
for i, name in enumerate(vertex_names):
    g.add_vertex_data(i, name)

g.add_edge(0, 1, 3)  # s  -> v1, cap: 3
g.add_edge(0, 2, 7)  # s  -> v2, cap: 7
g.add_edge(1, 3, 3)  # v1 -> v3, cap: 3
g.add_edge(1, 4, 4)  # v1 -> v4, cap: 4
g.add_edge(2, 1, 5)  # v2 -> v1, cap: 5
g.add_edge(2, 4, 3)  # v2 -> v4, cap: 3
g.add_edge(3, 4, 3)  # v3 -> v4, cap: 3
g.add_edge(3, 5, 2)  # v3 -> t,  cap: 2
g.add_edge(4, 5, 6)  # v4 -> t,  cap: 6

source = 0; sink = 5
print("The maximum possible flow is %d " % g.edmonds_karp(source, sink))
运行示例 »

Edmonds-Karp 算法的时间复杂度

Edmonds-Karp 和 Ford-Fulkerson 的区别在于,Edmonds-Karp 使用广度优先搜索 (BFS) 查找增广路径,而 Ford-Fulkerson 使用深度优先搜索 (DFS)。

这意味着 Edmonds-Karp 的运行时间比 Ford-Fulkerson 更易于预测,因为 Edmonds-Karp 不受最大流量值的影响。

设顶点数为 \(V\),边数为 \(E\),Edmonds-Karp 算法的时间复杂度为

\[ O(V \cdot E^2) \]

这意味着 Edmonds-Karp 不像 Ford-Fulkerson 那样依赖于最大流量,而是依赖于顶点和边的数量。

我们得到 Edmonds-Karp 的这种时间复杂度的原因为,它运行 BFS,BFS 的时间复杂度为 \(O(E+V)\)。

但是,如果我们假设 Edmonds-Karp 的最坏情况,即稠密图,其中边数 \(E\) 远大于顶点数 \(V\),BFS 的时间复杂度变为 \(O(E)\)。

对于每条增广路径,BFS 必须运行一次,并且在运行 Edmonds-Karp 算法期间,实际上可以找到接近 \(V \cdot E \) 条增广路径。

因此,在最坏情况下,时间复杂度为 \(O(E)\) 的 BFS 可以运行接近 \(V \cdot E \) 次,这意味着我们得到 Edmonds-Karp 的总时间复杂度: \( O(V \cdot E \cdot E) = O(V \cdot E^2) \)。



×

Contact Sales

If you want to use W3Schools services as an educational institution, team or enterprise, send us an e-mail:
[email protected]

Report Error

If you want to report an error, or if you want to make a suggestion, send us an e-mail:
[email protected]

W3Schools is optimized for learning and training. Examples might be simplified to improve reading and learning. Tutorials, references, and examples are constantly reviewed to avoid errors, but we cannot warrant full correctness of all content. While using W3Schools, you agree to have read and accepted our terms of use, cookie and privacy policy.

Copyright 1999-2024 by Refsnes Data. All Rights Reserved. W3Schools is Powered by W3.CSS.