网络流问题以及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\}
v∈V−{s,t}到
s
s
s的最短距离
d
[
v
]
d[v]
d[v]增加。
采用反证法,假设存在一个点
v
∈
V
−
{
s
,
t
}
v\in V-\{s,t\}
v∈V−{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
2∣v∣−1次。
对于关键边
(
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
∣V∣−2,所以每条边最多做关键边
∣
V
∣
2
−
1
\frac{|V|}{2}-1
2∣V∣−1次,总的增广次数就为
O
(
V
E
)
O(VE)
O(VE).
所以采用BFS进行增广的话, EK算法将达到复杂度
O
(
V
E
2
)
O(VE^2)
O(VE2)
但实际情况中,EK算法的复杂度远低于理论上的复杂度。