Edmonds-Karp详解-基于BFS的最短增广路径


网络流问题的求解算法中,Edmonds-Karp算法作为一种经典且高效的方法,在资源分配、网络通信等众多领域发挥着关键作用。它是Ford-Fulkerson算法的优化版本,通过利用广度优先搜索(BFS)寻找最短增广路径,有效避免了Ford-Fulkerson算法可能出现的低效情况,大幅提升了算法性能。本文我将详细阐述Edmonds-Karp算法的原理、实现流程,并用Python、C++和Java三种语言进行代码实现,带你全面深入地掌握这一重要算法。

一、网络流问题与相关概念回顾

1.1 网络流问题定义

网络流问题可抽象为一个有向图 G = ( V , E ) G=(V, E) G=(V,E),其中 V V V 是顶点集合, E E E 是边集合。图中存在一个源点 s s s 和一个汇点 t t t,每条边 ( u , v ) ∈ E (u, v) \in E (u,v)E 都有一个非负的容量 c ( u , v ) c(u, v) c(u,v),表示该边能够传输的最大流量。网络流问题的核心目标是在满足流量守恒原则(除源点和汇点外,每个顶点的流入流量等于流出流量)的前提下,找到从源点 s s s 到汇点 t t t 的最大流量。

1.2 关键概念

  • 流量:对于边 ( u , v ) (u, v) (u,v),其流量 f ( u , v ) f(u, v) f(u,v) 表示实际通过该边的流量,需满足 0 ≤ f ( u , v ) ≤ c ( u , v ) 0 \leq f(u, v) \leq c(u, v) 0f(u,v)c(u,v)。同时,除源点和汇点外,任意顶点 v v v 都应满足 ∑ u : ( u , v ) ∈ E f ( u , v ) = ∑ w : ( v , w ) ∈ E f ( v , w ) \sum_{u:(u,v)\in E} f(u, v) = \sum_{w:(v,w)\in E} f(v, w) u:(u,v)Ef(u,v)=w:(v,w)Ef(v,w),即流入流量等于流出流量。
  • 残留网络:给定网络 G G G 和流量 f f f,残留网络 G f G_f Gf 由原网络顶点和残留边组成。残留边 ( u , v ) (u, v) (u,v) 的容量 c f ( u , v ) c_f(u, v) cf(u,v) 定义如下:
    • ( u , v ) ∈ E (u, v) \in E (u,v)E,则 c f ( u , v ) = c ( u , v ) − f ( u , v ) c_f(u, v) = c(u, v) - f(u, v) cf(u,v)=c(u,v)f(u,v),表示该边还能容纳的额外流量。
    • ( v , u ) ∈ E (v, u) \in E (v,u)E,则 c f ( u , v ) = f ( v , u ) c_f(u, v) = f(v, u) cf(u,v)=f(v,u),意味着可以通过反向边撤销已流过的流量。
    • ( u , v ) ∉ E (u, v) \notin E (u,v)/E ( v , u ) ∉ E (v, u) \notin E (v,u)/E,则 c f ( u , v ) = 0 c_f(u, v) = 0 cf(u,v)=0
  • 增广路径:在残留网络 G f G_f Gf 中,从源点 s s s 到汇点 t t t 的简单路径称为增广路径。沿着增广路径可以增加从源点到汇点的流量,增广路径上所有残留边的最小容量就是可增加的流量值。

二、Edmonds-Karp算法原理

2.1 算法核心思想

Edmonds-Karp算法基于Ford-Fulkerson算法框架,其核心思想是在每次迭代中,利用广度优先搜索(BFS)在残留网络中寻找从源点 s s s 到汇点 t t t 的最短增广路径(这里的最短指边数最少),然后沿着该路径增加流量,更新残留网络。重复此过程,直到残留网络中不存在从源点到汇点的增广路径,此时得到的流量即为网络的最大流。通过优先选择最短增广路径,Edmonds-Karp算法避免了Ford-Fulkerson算法因选择长增广路径导致的低效情况,保证了算法的高效性和有限次迭代收敛。

2.2 算法具体流程

  1. 初始化:创建原始网络 G G G,确定源点 s s s 和汇点 t t t,将所有边的流量 f ( u , v ) f(u, v) f(u,v) 初始化为 0,构建初始的残留网络 G f G_f Gf。同时,创建一个用于记录增广路径的数组(或列表),用于存储每个顶点在增广路径上的前驱顶点。
  2. BFS寻找最短增广路径:使用BFS在残留网络 G f G_f Gf 中从源点 s s s 开始搜索,尝试找到一条到达汇点 t t t 的最短增广路径。在搜索过程中,记录每个顶点的前驱顶点,以便后续回溯得到增广路径。如果找到增广路径,则进入步骤3;若找不到,说明已达到最大流,算法结束,返回当前流量作为最大流。
  3. 计算可增加的流量:沿着找到的增广路径,计算路径上所有残留边的最小容量 δ \delta δ δ \delta δ 即为可以沿着该增广路径增加的流量值。
  4. 更新流量和残留网络:沿着增广路径更新原始网络中边的流量 f ( u , v ) f(u, v) f(u,v) 和残留网络中边的容量 c f ( u , v ) c_f(u, v) cf(u,v)。对于增广路径上的边 ( u , v ) (u, v) (u,v)
    • 在原始网络中,若 ( u , v ) (u, v) (u,v) 是正向边,则 f ( u , v ) = f ( u , v ) + δ f(u, v) = f(u, v) + \delta f(u,v)=f(u,v)+δ;若 ( u , v ) (u, v) (u,v) 是反向边,则 f ( u , v ) = f ( u , v ) − δ f(u, v) = f(u, v) - \delta f(u,v)=f(u,v)δ
    • 在残留网络中,相应地更新边的容量 c f ( u , v ) c_f(u, v) cf(u,v),以反映流量的变化。
  5. 重复步骤:返回步骤2,继续在更新后的残留网络中寻找最短增广路径,直到找不到增广路径为止。
    Edmonds-karp

三、Edmonds-Karp算法的代码实现

3.1 Python实现

from collections import deque


def edmonds_karp(graph, source, sink):
    rows = len(graph)
    residual_graph = [[graph[i][j] for j in range(rows)] for i in range(rows)]
    parent = [-1] * rows
    max_flow = 0

    while True:
        queue = deque()
        queue.append(source)
        visited = [False] * rows
        visited[source] = True

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

        if not visited[sink]:
            break

        path_flow = float("Inf")
        s = sink
        while s != source:
            path_flow = min(path_flow, residual_graph[parent[s]][s])
            s = parent[s]

        max_flow += path_flow
        v = sink
        while v != source:
            u = parent[v]
            residual_graph[u][v] -= path_flow
            residual_graph[v][u] += path_flow
            v = parent[v]

    return max_flow


# 示例图
graph = [
    [0, 16, 13, 0, 0, 0],
    [0, 0, 10, 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]
]
source = 0
sink = 5
print(edmonds_karp(graph, source, sink))

上述Python代码中,edmonds_karp 函数实现了Edmonds-Karp算法。首先初始化残留网络和记录前驱顶点的数组,然后通过一个无限循环不断使用BFS寻找增广路径。找到增广路径后,计算可增加的流量并更新流量和残留网络,直到找不到增广路径,最终返回最大流。

3.2 C++实现

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

bool bfs(vector<vector<int>>& residual_graph, int source, int sink, vector<int>& parent) {
    vector<bool> visited(residual_graph.size(), false);
    queue<int> q;
    q.push(source);
    visited[source] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int ind = 0; ind < residual_graph.size(); ind++) {
            if (!visited[ind] && residual_graph[u][ind] > 0) {
                q.push(ind);
                visited[ind] = true;
                parent[ind] = u;
            }
        }
    }

    return visited[sink];
}

int edmonds_karp(vector<vector<int>>& graph, int source, int sink) {
    int n = graph.size();
    vector<vector<int>> residual_graph = graph;
    vector<int> parent(n);
    int max_flow = 0;

    while (bfs(residual_graph, source, sink, parent)) {
        int path_flow = INT_MAX;
        int v = sink;

        while (v != source) {
            path_flow = min(path_flow, residual_graph[parent[v]][v]);
            v = parent[v];
        }

        max_flow += path_flow;
        v = sink;

        while (v != source) {
            int u = parent[v];
            residual_graph[u][v] -= path_flow;
            residual_graph[v][u] += path_flow;
            v = parent[v];
        }
    }

    return max_flow;
}

int main() {
    vector<vector<int>> graph = {
        {0, 16, 13, 0, 0, 0},
        {0, 0, 10, 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 source = 0;
    int sink = 5;
    cout << edmonds_karp(graph, source, sink) << endl;
    return 0;
}

C++代码中,bfs 函数负责在残留网络中使用BFS寻找增广路径,并记录路径上的前驱顶点。edmonds_karp 函数实现了算法的整体流程,包括初始化、调用 bfs 寻找增广路径、计算流量并更新残留网络,直至找不到增广路径,最后返回最大流。

3.3 Java实现

import java.util.LinkedList;
import java.util.Queue;

class EdmondsKarp {
    static boolean bfs(int[][] residualGraph, int source, int sink, int[] parent) {
        boolean[] visited = new boolean[residualGraph.length];
        Queue<Integer> queue = new LinkedList<>();
        queue.add(source);
        visited[source] = true;

        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (int ind = 0; ind < residualGraph.length; ind++) {
                if (!visited[ind] && residualGraph[u][ind] > 0) {
                    queue.add(ind);
                    visited[ind] = true;
                    parent[ind] = u;
                }
            }
        }

        return visited[sink];
    }

    static int edmondsKarp(int[][] graph, int source, int sink) {
        int n = graph.length;
        int[][] residualGraph = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                residualGraph[i][j] = graph[i][j];
            }
        }
        int[] parent = new int[n];
        int maxFlow = 0;

        while (bfs(residualGraph, source, sink, parent)) {
            int pathFlow = Integer.MAX_VALUE;
            int v = sink;

            while (v != source) {
                pathFlow = Math.min(pathFlow, residualGraph[parent[v]][v]);
                v = parent[v];
            }

            maxFlow += pathFlow;
            v = sink;

            while (v != source) {
                int u = parent[v];
                residualGraph[u][v] -= pathFlow;
                residualGraph[v][u] += pathFlow;
                v = parent[v];
            }
        }

        return maxFlow;
    }
}

public class Main {
    public static void main(String[] args) {
        int[][] graph = {
            {0, 16, 13, 0, 0, 0},
            {0, 0, 10, 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 source = 0;
        int sink = 5;
        System.out.println(EdmondsKarp.edmondsKarp(graph, source, sink));
    }
}

Java代码中,bfs 方法用于在残留网络中通过BFS寻找增广路径,并记录前驱顶点。edmondsKarp 方法实现了Edmonds-Karp算法的完整逻辑,包括初始化残留网络、调用 bfs 寻找增广路径、更新流量和残留网络,直至无法找到增广路径,最终返回最大流。

四、Edmonds-Karp算法的时间复杂度与空间复杂度分析

4.1 时间复杂度

Edmonds-Karp算法的时间复杂度分析如下:

  • 每次使用BFS寻找最短增广路径的时间复杂度为 O ( V + E ) O(V + E) O(V+E),其中 V V V 是顶点数量, E E E 是边的数量。
  • 由于每次沿着最短增广路径增加流量后,至少会使某条边的流量达到其容量上限(即某条边在残留网络中消失或出现反向边),而边的数量为 E E E,所以增广路径的数量最多为 O ( V E ) O(VE) O(VE) 次。
  • 综合以上两点,Edmonds-Karp算法的时间复杂度为 O ( V E 2 ) O(VE^2) O(VE2)。相比Ford-Fulkerson算法在最坏情况下的时间复杂度 O ( ∣ f ∗ ∣ ( V + E ) ) O(|f^*|(V + E)) O(f(V+E)) ∣ f ∗ ∣ |f^*| f 为最大流的值),Edmonds-Karp算法通过选择最短增广路径,保证了更优的时间复杂度,使其在大多数情况下具有更好的性能表现。

4.2 空间复杂度

Edmonds-Karp算法的空间复杂度主要取决于存储网络和残留网络以及辅助数据结构所需的空间:

  • 存储网络和残留网络:如果使用邻接矩阵存储,空间复杂度为 O ( V 2 ) O(V^2) O(V2);若使用邻接表存储,空间复杂度为 O ( V + E ) O(V + E) O(V+E)。通常在实际应用中,邻接表更适合存储稀疏图,空间效率更高。
  • 辅助数据结构:算法在执行过程中,需要使用队列存储BFS搜索过程中的顶点,以及数组(或列表)记录增广路径上的前驱顶点等辅助数据。队列在最坏情况下可能存储所有顶点,空间复杂度为 O ( V ) O(V) O(V);记录前驱顶点的数组空间复杂度也为 O ( V ) O(V) O(V)。因此,辅助数据结构的空间复杂度为 O ( V ) O(V) O(V)
  • 综合起来,在使用邻接表存储网络时,Edmonds-Karp算法的空间复杂度为 O ( V + E ) O(V + E) O(V+E)

五、Edmonds-Karp算法的应用场景

5.1 资源分配

在资源分配场景中,如工厂生产资源分配、云计算资源分配等,可将资源的生产者视为源点,资源的消费者视为汇点,资源的传输路径视为边,边的容量表示路径能够传输的资源量上限。通过Edmonds-Karp算法可以找到一种最优的资源分配方案,使得从生产者到消费者的资源传输总量最大,从而实现资源的高效利用和合理分配。

5.2 网络通信

在计算机网络通信中,数据从源节点传输到目标节点,网络中的链路可看作边,链路的带宽可看作边的容量。利用Edmonds-Karp算法能够计算出在给定网络拓扑和链路带宽的情况下,从源节点到目标节点的最大数据传输量。这有助于网络管理员优化网络拓扑结构、合理分配带宽资源,提高网络的传输效率和稳定性,避免网络拥塞。

5.3 交通运输

在城市交通系统中,道路可视为边,道路的通行能力可视为边的容量,城市中的某些关键地点(如交通枢纽)可作为源点和汇点。以北京为例,将国贸、西直门等交通枢纽设为源点,将郊区物流中心设为汇点,通过 Edmonds-Karp 算法分析城市交通网络的最大通行流量,能够为交通规划提供重要依据。具体而言,算法可通过识别网络中的 “瓶颈路段”,即限制整体流量的关键道路,为交通规划提供关键决策支持:例如确定需要拓宽哪些路段来提升整体通行效率;规划地铁、高架桥等新交通设施的最佳建设位置,以分担现有道路的压力;甚至可以优化信号灯配时方案,让道路资源得到更高效的利用。此外,通过动态调整源点和汇点,算法还能模拟不同区域在高峰时段的流量变化,帮助制定更科学的潮汐车道管理策略。

总结​

作为网络流问题的经典算法,Edmonds-Karp 基于广度优先搜索(BFS)策略寻找最短增广路径,相比传统 Ford-Fulkerson 方法,在时间复杂度上实现了显著优化。

That’s all, thanks for reading!
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值