最大流: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 的最小值。

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

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

image-20231007153135516

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

为什么要反向建边

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

增广路

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

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

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

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

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

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

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

EK 算法

时间复杂度:O(n2m)

首先考虑的是怎么找增广路径,先前说用搜索算法,可以用 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(n2m)

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 @   straySheep  阅读(243)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示