最大流:EK、Dinic 算法

最大流:EK、Dinic 算法

首先做出一些定义,给定一个图 G(V, E),这里 V 时图上顶点集合,E 是有向边的集合,在实际应用中,E 中任意一条边都包含:头,尾,容量 cap,流量 flow,4 个属性。

  • cap 指定了当前边能够通过的最大流量(定值)。
  • flow 则记录了当前边通过的流量(初始为 0,表示没有流量通过)。

另外会定义两个特殊的点,源点 s 和 汇点 t,分别代表网络流的流入节点和流出节点。

可行流 满足:

  • 源点 s:流出量 = 整个网络的流量。
  • 汇点 t:流入量 = 整个网络的流量。
  • 中间点:总流入量 = 总流出量。

例如:

image-20231007124511086

红色部分表示流量 flow,黑色部分表示容量 cap。可行流为 7。

最大流

  • 所有可行流中流量最大的流量。
  • 最大流可能不止一个。

反向边

首先要知道,在一条从源点 s 到汇点 t 的路径中,能够带来的最大流量取决于这条路径上的最小容量。

image-20231007154842565

如上所示,如果我们使用搜索算法找到一条从 S 到 T 的路径,并且这条路径能够带来的流量就是这条路径上边的容量 cap 的最小值。

假设找到的路径是 \(S \rightarrow2 \rightarrow 3 \rightarrow T\)。现在的流量是 1,因为这条路边已经使用过了,故把这条路径上的每条边 cap 减去流量 flow=1,再次找从 S 到 T 的路径,发现找不到了,因此得到的 ans 为 1,但是正确的 ans 应该是 2。即 \(S \rightarrow 2 \rightarrow T\)\(S \rightarrow 3 \rightarrow T\)

这个时候,为了能够继续找到路径,必须采用反向建边,把当前这条找到的路径进行反向建边,边的权值就是这条路径的流量 flow。如下所示:

image-20231007153135516

其中红色的边为反向边。这时继续寻找路径,发现可以走 \(S \rightarrow 3 \rightarrow 2 \rightarrow T\),带来的流量是 1,然后继续找寻路径,发现没有可达的路径了,因此最大流是 \(1 + 1 = 2\)

为什么要反向建边

仔细想想应该能够明白,反向建边的作用相当于让之前的路径可以有反悔的余地。这样即使一开始走错也没关系,因为可以通过反向边来反悔,使得最终一定能够得到正确的 ans。

增广路

最大流的核心在于寻找增广路。

如果一个可行流不是最大流,那么当前网络中一定还存在增广路径。

从源点 S 到汇点 T 的一条路径中,如果边 \(<u, v>\) 与该路径的方向一致就称为正向边,否则就记为逆向边。此外,若在这条路径上的所有边满足:

  • 正向边 \(f(u, v) < c(u, v)\)
  • 逆向边 \(f(u, v) > 0\)

则该路径是增广路径。即增广路径是这样一条路径,可以用来增加到达汇点的流量,并且路径中的流量没有超过每条边的 cap。

沿这条增广路改进可行流的操作称为增广,而所有可能的增广路径放在一起就构成了残余网络。

以下两个算法,都是基于不断找增广路径来实现的。

EK 算法

时间复杂度:\(O(n^2m)\)

首先考虑的是怎么找增广路径,先前说用搜索算法,可以用 bfs (dfs)。但是 bfs 的好处在于能够在残余网络中每一次找到最短的一条增广路径。因此EK算法是基于 bfs 来找增广路径的,bfs 每执行一次,就找出一条增广路,然后把这条路径上的权值进行修改,同时反向建边。直到找不到增广路径为止。

过程:

  • 采用邻接矩阵存储图。(通常情况下,都是稠密图,故用邻接矩阵存储)

  • 利用 bfs 每次找到一条最短增广路径,记录当前该路径的最小流量 inc。

  • 更新邻接矩阵这条路径上的流量。

  • 不断重复 2,3 直到没有增广为止。

模板题:P2740 [USACO4.2] 草地排水Drainage Ditches

def solve():
    m, n = map(int, readline().split())

    adj = [[0] * (n + 1) for _ in range(n + 1)]
    for i in range(m):
        x, y, c = map(int, readline().split())
        adj[x][y] += c

    pre = defaultdict(lambda: int(-1))
    def bfs(u: int) -> int:
        nonlocal pre
        pre = defaultdict(lambda: int(-1))
        dist = [0 for _ in range(n + 1)]
        dist[u] = float('inf')

        q = deque()
        q.append(s)
        while q:
            x = q.popleft()
            if x == t: break # 找到了汇点
            for y in range(1, n + 1):
                if pre[y] == -1 and adj[x][y] > 0: # 找到一个没有访问,且还有 cap 的点
                    pre[y] = x
                    dist[y] = min(dist[x], adj[x][y]) # 更新增广路的最小流量
                    q.append(y)
            
        return (dist[t] if pre[t] != -1 else -1)

    def EK() -> int:
        res = 0 
        while True:
            inc = bfs(s)
            if inc == -1: break
            y = t
            while y != s:
                x = pre[y]
                adj[x][y] -= inc # 正向边容量减去流量
                adj[y][x] += inc # 逆向边建立
                y = pre[y]
            res += inc
        return res

    s, t = 1, n
    ans = EK()
    print(ans)

Dinic算法

时间复杂度:\(O(n^2m)\)

EK 算法找增广路径是基于 bfs 来进行的, bfs 会把周围能够扩展的点全部扩展进来,直到找到汇点为止,相当于每找一次增广路径都要搜索大量的点。 Dinic 算法实际上是对 EK 的优化。

Dinic 算法利用 bfs 建立分层网络(按照每个点到源点的距离进行分层)。然后基于这个分层网络,利用 dfs 找到当前分层网络下的所有增广路径,并做好相应的修改。之后不断重复这两个过程,直到无法分层为止,这样做只需要一次 bfs 就可以找到一簇增广路径。

所谓分层网络,就是利用 bfs 特性,以源点为起点,一直向外扩散。每经过一个点都打一个标记,标记到源点的路径所经过的最少的边的数量,假设用 dist[i] 表示 i 到源点 s 的所经过的边的数量,这样就可以将整个网络分层。基于这个分层网络,dfs 在找增广路时,就可以找到最短的增广路径。如果当前点是 x,dfs 搜到下一个点 y,一定要满足 dist[y] = dist[x] + 1,这样才是最短的增广路径,效率才是最高的。

过程:

  • 采用邻接矩阵存储图。

  • 利用 bfs 建立分层网络(记录每个节点的深度)。

  • 按照当前分层网络进行 dfs (一层一层找),找到所有该分层网络下的增广路径,并更新残余网络)。

  • 重复 1,2 直到无法建立分层网络为止。

def solve():
    m, n = map(int, readline().split())

    adj = [[0] * (n + 1) for _ in range(n + 1)]
    for i in range(m):
        x, y, c = map(int, readline().split())
        adj[x][y] += c

    dep = [float('inf') for _ in range(n + 1)]
    def bfs() -> int:
        nonlocal dep
        dep = [float('inf') for _ in range(n + 1)]
        dep[s] = 0

        q = deque()
        q.append(s)
        while q:
            x = q.popleft()
            for y in range(1, n + 1):
                if adj[x][y] > 0 and dep[y] > dep[x] + 1:
                    dep[y] = dep[x] + 1
                    q.append(y)

        return (True if dep[t] < float('inf') else False)

    def dfs(x: int, mn: int) -> int:
        inc = 0
        if x == t: return mn
        for y in range(1, n + 1):
            if adj[x][y] > 0 and dep[y] == dep[x] + 1:
                inc = dfs(y, min(mn, adj[x][y]))
                if inc > 0:
                    adj[x][y] -= inc
                    adj[y][x] += inc
                    return inc
        return 0
    def dinic() -> int:
        res = 0
        while bfs():
            res += dfs(s, inf)
        return res

    s, t = 1, n
    ans = dinic()
    print(ans)

【References】

posted @ 2023-10-07 18:47  straySheep  阅读(764)  评论(0)    收藏  举报