网络流问题以及EK算法复杂度分析

网络流问题以及EK算法复杂度分析

一、网络流算法

通过一个例子引入网络流问题。
现有一个自来水厂要往家里通水,自来水厂用Vs表示,家用Vt表示。从自来水厂到家之间连接了很多水管,并且中途经过很多转接点。每根水管都有粗细,通过水管的水流量有个上界,超过则会撑爆水管。现在问:从自来水厂防水到家里,一共能够汇集多少水。这就是网络流的最大流问题。
网络流问题示意

网络流问题:从源点(Vs)到汇点(Vt)所经过的所有路径,最终到达汇点的所有流量之和。
网络流算法就是为了求解最大流问题。
对于一个流网络G = (V, E), 容量函数为c,源点为Vs,汇点为Vt,G的可行流f满足如下三个性质:

  • 容量限制:对所有的u,v∈V,要求f(u,v)<=c(u,v)。
  • 反对称性:对所有的u,v∈V,要求f(u,v)=-f(v,u)。
  • 流守恒性:对所有u∈V-{s,t},要求∑f(u,v)=0 (v∈V)。

二、增广路定理

定义每条边(u,v)有一个容量c(u,v),流量 f(u,v), 残量r(u,v) = c(u,v)-f(u,v).
r(u, v) 构成残留网络。在残量网络上寻找到一条可行流,称为增广路
当残留网络中没有增广路时,网络流达到最大流。
下面我们通过几张图来看一下找可行流的过程。
(1)首先这是一张网络流图。
在这里插入图片描述

(2)假设我们找到一条可行流 :1——2——3——4,流量为2,所以在这几条边上的容量变为0。
在这里插入图片描述
这个时候我们发现在图中再也找不到可行流了。也就是最大流为2。
但是如果我么选择的是1——2——4和1——3——4两条可行流,那么最大流应该是2+1=3,所以这个时候我们需要有办法重新分配流量的路线。
这就引入了网络流算法的精髓之处:增加反向边。
(3)增加反向边
在这里插入图片描述

(3)增加反向边之后,找到路径:1——3——2——4.流量为1,最大流为2+1=3.
在这里插入图片描述

关于为什么要增加反向边,很多博客里写的都是那句话,“给程序一个反悔的机会,把分错的流量撤销后进行重新分配。”
这样说其实不好理解。很多人可能会冒出和我一样的问题,就是为什么可以平白无故增加反向边,认为流量可以逆流。
我个人的理解是,流量不是逆流,我们是在找一种新的分配方式,就是流量如果有两个出口,我们可以对两个出口进行分配。如果我们把2的流量(2份)全部分配到3,不是最优解,所以我们就得尝试分1份分配到3,剩下一份继续分配,发现可以分配到4。所以汇集到4的流量,1份来源于2流出流量的再分配(1份到3再到4,另一份直接到4),还有一份来源于3。

三、Edmons-karp算法(EK算法)

  • 从源点开始,用BFS找一条最短的增广路径,计算该路径上的残量最小值,累加到最大流值;
  • 沿着该路径修改流量值,实际是修改是残量网络的边权;
  • 重复上述步骤,直到找不到增广路时,此时得到的流就是最大流。

我们用图来演示算法的运行过程:
initialize :

  • graph 存图;
  • pre:前驱数组,记录前驱点;
  • d:记录路径中最小流量;
  • visit:记录该点是否被访问;
  • flow:记录最大流量;
  • queue:bfs所用队列,找路径。

**step1:queue = [], pre[1] = -1, maxflow = 0

在这里插入图片描述

step2: 访问点1,加入队列,queue =[1], pre[1] = -1
在这里插入图片描述
step3: 1出队列,2,3入队,queue=[2,3], pre[2] = 1, pre[3] = 1
在这里插入图片描述
step4:2出队列,4入队列,queue=[3,4]. pre[4] = 2
得到路径:1–>2–>4

在这里插入图片描述
step5: 修改该路径上的边权,增加反向边, d(4,2) = 2, d(2,1) = 2, dmin = 2,maxflow = 2
在这里插入图片描述
**找到路径:1–>3–>4,dmin = 1, maxflow = 2+1 = 3
在这里插入图片描述
四、代码实现

#usr/bin/python3
#最大流-EK算法
def bfs(graph, s, t):
    global visit
    global pre
    n = len(graph)
    visit = [False for i in range(n)] #记录是否访问
    pre = {}
    queue = []
    pre[s] = -1
    visit[s] = True
    print('visit:', visit)
    queue.append(s)
    while queue:
        p = queue.pop(0)
        for i in range(len(graph[p])):
            #print('p:',p)
            if graph[p][i] > 0 and visit[i] == False:
                pre[i] = p
                visit[i] = True
                print(p, i)
                if i == t:
                    print("找到一条增广路径")
                    print("visit:", visit)
                    return True
                queue.append(i)
    return False


def EdomonsKarp(graph, s, t):
    print('s:',s,'t:',t)
    global flow
    global d
    d = 0
    flow = 0
    while bfs(graph, s, t):
        d = graph[pre[t]][t]
        x = t 
        while pre[x] != -1:
            print('点:', x, '前驱点:', pre[x])
            d = min(graph[pre[x]][x], d)
            print("从点",x,"到点",pre[x],'流量最小值为', d)
            x = pre[x]
        x = t
        while pre[x] != -1:
            graph[pre[x]][x] -= d
            graph[x][pre[x]] += d 
            x = pre[x]
            print('graph:', graph)
        flow += d
        print(flow)
    
    return flow

if __name__ == "__main__":
    graph = [
        [0, 2, 2, 0],
        [0, 0, 2, 2],
        [0, 0, 0, 2],
        [0, 0, 0, 0]
        ]
    n = len(graph)
    visit = [False for i in range(n)] #记录是否访问
    #pre = {} #记录前驱点
    s = 0
    t = 3
    flow = EdomonsKarp(graph, s, t)
    print(flow)
                

广度优先搜索:如果采用邻接矩阵作为图的存储结构,时间复杂度为O(V*2),如果采用邻接表作为图的存储结构,时间复杂度为O(V+E)
深度优先搜索:和上述一样
解释:广度优先搜索的时间复杂度分析,由于每个节点仅被发现一次,因此每个节点入栈和出栈各一次,时间复杂度均为O(1),故入栈和出栈的总时间为O(V),最坏的情况下,需要对每个节点的邻接点进行扫描,所以时间复杂度为O(E),在此之后,每条边至少访问一次,这是因为在搜索的过程中,若某个节点向下搜索时,其子节点都访问过了,这时候就会回退,所以时间复杂度为O(E),所以总的时间复杂度为O(V+E);
邻接矩阵存储方式时,查找每个顶点的邻接点所需时间为O(V),又有n个顶点,所以时间复杂度为O(V^2).

五、EK算法时间复杂度分析

引理:EK算法每次增广都会使得所有顶点 v ∈ V − { s , t } v\in V - \{s,t\} vV{s,t} s s s的最短距离 d [ v ] d[v] d[v]增加。
采用反证法,假设存在一个点 v ∈ V − { s , t } v\in V-\{s,t\} vV{s,t},使得 d ′ [ v ] < d [ v ] d'[v] < d[v] d[v]<d[v]。v的前驱点为u。
因此可以得到
d [ u ] = d [ v ] − 1 , d ′ [ u ] > = d [ u ] d[u] = d[v]-1, d'[u] >= d[u] d[u]=d[v]1,d[u]>=d[u]
那么显然边 ( u , v ) i̸ n E (u,v)\not in E (u,v)inE, 因为如果 ( u , v ) ∈ E (u,v) \in E (u,v)E,则一定有
d [ v ] < = d [ u ] + 1 < = d ′ [ u ] + 1 = d ′ [ v ] d[v] <= d[u]+1 <= d'[u]+1 = d'[v] d[v]<=d[u]+1<=d[u]+1=d[v]
与假设矛盾。
所以EK算法一定是增加了流 f ( u , v ) f(u,v) f(u,v),即边 ( v , u ) (v,u) (v,u) G G G的最短路上,固有,
d [ v ] = d [ u ] − 1 < = d ′ [ u ] − 1 = d ′ [ v ] − 2 d[v] = d[u] - 1 <= d'[u] - 1 = d'[v]-2 d[v]=d[u]1<=d[u]1=d[v]2
与假设矛盾所以引理成立。

定理:EK算法的最多增广次数为 O ( V E ) O(VE) O(VE)
若增广路 p p p的残留容量等于边 ( u , v ) (u,v) (u,v)的残留容量,则称边 ( u , v ) (u,v) (u,v)是增广路 p p p的关键边,下面用引理证明每条边最多做关键边 ∣ v ∣ 2 − 1 \frac{|v|}{2}-1 2v1次。

对于关键边 ( u , v ) (u,v) (u,v),由于 ( u , v ) (u,v) (u,v)在最短路上,有
d [ v ] = d [ u ] + 1 d[v] = d[u] + 1 d[v]=d[u]+1
而增广后, ( u , v ) (u,v) (u,v)将会从 G G G中消失,重新出现的条件是 ( v , u ) (v,u) (v,u)出现在增广路上。那么则有
d ′ [ u ] = d ′ [ v ] + 1 d'[u] = d'[v] + 1 d[u]=d[v]+1
由引理我们知道
d ′ [ v ] > = d [ v ] d'[v] >= d[v] d[v]>=d[v]
故有
d ′ [ u ] > = d [ v ] + 1 = d [ u ] + 2 d'[u] >= d[v] + 1 = d[u] + 2 d[u]>=d[v]+1=d[u]+2
所以每次出现至少会使得最短距离 + 2 +2 +2,而其距离最大为 ∣ V ∣ − 2 |V|-2 V2,所以每条边最多做关键边 ∣ V ∣ 2 − 1 \frac{|V|}{2}-1 2V1次,总的增广次数就为 O ( V E ) O(VE) O(VE).

所以采用BFS进行增广的话, EK算法将达到复杂度 O ( V E 2 ) O(VE^2) O(VE2)
但实际情况中,EK算法的复杂度远低于理论上的复杂度。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值