线性规划问题有一个有趣的特性,即所有求极大的问题都有一个与其匹配的求极小的线性规划问题。我们通过求解一个问题的对偶问题,再加以转化就可以得到原始问题的解
G(V, E)
shortest Path Fast Algorithim
)寻找cost最小的一个路径minFlow
minFlow
, 同时对应反向边加上minFlow
False
minFlow
相加就是最终的maxFlow
, 将每次的路径的费用相加即为最小费用。算法核心:
G(V, E)
GVector
内部类-节点Edge
内部类-边 保存 from, to, cap, cost, antiIndex(反向边在所在节点位置)Graph
: {vIdex: List[Edge]}
邻接表 vIdex和v_list相对应v_list
: List[GVector]
节点列表cap > 0 and cost_list[t] > cost_list[now] + cst
visted_list
: 记录节点是否被访问,防止死循环,以及有效进行扩增cost_list
: 记录起点到当前节点路径的累积costpre_vector
: 路径节点记录,pre_vector[now]
:表示为到达now
节点的上个节点pre_vector_edge
:路径边记录,pre_vector_edge[now]
:表示为通过该边到达now
节点-minflow
,对应边反向边流量+minflow
)完整代码见笔者github: MCMF.py (记得顺手点star哦)
from collections import defaultdict, deque
from typing import List, Union, Any, AnyStr, Dict
import math
from rich.console import Console
cs = Console()
class MCMF:
....
def spfa(self) -> bool:
st = self.st_idx
ed = self.ed_idx
# 记录起点到当前节点路径的累积cost
cost_list = [math.inf] * self.num_nodes
# 记录节点是否访问
visted_list = [False] * self.num_nodes
# 刷新辅助记录栈
self.pre_vector = [-1] * self.num_nodes
self.pre_vector_edge = [-1] * self.num_nodes
# 辅助queue, 进行fbs
queue = deque()
queue.append(st)
cost_list[st] = 0
visted_list[st] = True
while len(queue):
now = queue.popleft()
visted_list[now] = False
for e_idx, edge_i in enumerate(self.graph[now]):
f = edge_i.f
t = edge_i.to
cst = edge_i.cost
cap = edge_i.cap
if cap > 0 and cost_list[t] > cost_list[now] + cst:
cost_list[t] = cost_list[now] + cst
self.pre_vector[t] = now
self.pre_vector_edge[t] = e_idx
if ~visted_list[t]:
visted_list[t] = True
queue.append(t)
return cost_list[ed] < math.inf
def solve(self):
out = {
'flowPath': [],
'maxFlow': 0,
'minCost': 0
}
while self.spfa():
tmp = self.ed_idx
min_flow = math.inf
# 寻找路径流量
while tmp != self.st_idx:
# 反向轨迹
pre_v = self.pre_vector[tmp]
pre_v_e = self.pre_vector_edge[tmp]
min_flow = min(min_flow, self.graph[pre_v][pre_v_e].cap)
tmp = pre_v
tmp = self.ed_idx
path_min_cost = 0
# 更新图
while tmp != self.st_idx:
# 反向轨迹
pre_v = self.pre_vector[tmp]
pre_v_e = self.pre_vector_edge[tmp]
# 正向边
self.graph[pre_v][pre_v_e].cap -= min_flow
# 反向边
now_v_e = self.graph[pre_v][pre_v_e].antiIndex
self.graph[tmp][now_v_e].cap += min_flow
# 路径的cost
path_min_cost += self.graph[pre_v][pre_v_e].cost
tmp = pre_v
out['maxFlow'] += min_flow
out['minCost'] += path_min_cost * min_flow
out['flowPath'] = self.find_path()
return out
def find_path(self):
"""
找出最终有cost且有流量的边
"""
out = []
for v, e_list in self.graph.items():
for e in e_list:
if e.cost > 0 and e.cap != e.cap_org:
out.append(f'{e.f}->{e.to}')
return out
if __name__ == '__main__':
fList = [0, 0, 1, 1, 1, 2, 2, 3, 4]
toList = [1, 2, 2, 3, 4, 3, 4, 4, 2]
cost = [4, 4, 2, 2, 6, 1, 3, 2, 3]
cap = [15, 8, 20, 4, 10, 15, 4, 20, 5]
mcmf_opt = MCMF(fList, toList, cap, cost, 0, 4)
out = mcmf_opt.solve()
cs.print(out)
求解结果如下
--------------------------------------------------
[
normal(0-idx:0),
normal(1-idx:1),
normal(2-idx:2),
normal(3-idx:3),
normal(4-idx:4)
]
--------------------------------------------------
defaultdict(, {
1: [R(1 <- 0)[0/-4], F(1 -> 2)[20/2], F(1 -> 3)[4/2], F(1 -> 4)[10/6]],
0: [F(0 -> 1)[15/4], F(0 -> 2)[8/4]],
2: [
R(2 <- 0)[0/-4],
R(2 <- 1)[0/-2],
F(2 -> 3)[15/1],
F(2 -> 4)[4/3],
R(2 <- 4)[0/-3]
],
3: [R(3 <- 1)[0/-2], R(3 <- 2)[0/-1], F(3 -> 4)[20/2]],
4: [R(4 <- 1)[0/-6], R(4 <- 2)[0/-3], R(4 <- 3)[0/-2], F(4 -> 2)[5/3]]
})
--------------------------------------------------
{
'flowPath': ['1->2', '1->3', '0->1', '0->2', '2->3', '2->4', '3->4'],
'maxFlow': 23,
'minCost': 187
}
ortools 具两个优化器
SimpleMaxFlow
最大流SimpleMinCostFlow
流对应的最小费用只需要将两者以线性相结合就可以得到我们的最小费用最大流(MCMF)。
最小费用最大流的一般求解方法:
解的边集是唯一的
最大流的求一般解方法:
解的边集不是唯一的
所以,只要限制寻找的路径流量,寻找最小费用,那么也可以得到最小费用最大流。
只要限制的值是最大流(流量是唯一的,边集合不一定唯一),去寻找最小费用的边集
那么最终结果也将是一致。即组合SimpleMaxFlow
与SimpleMinCostFlow
就可得到最终的MCMF求解器
from ortools.graph import pywrapgraph
def ortoolsMCMF(from_list, to_list, caps, costs, start_node_idx, end_node_idx):
# 1- 初始化最大流优化器
max_flow = pywrapgraph.SimpleMaxFlow()
# 2- 最大流-图构建
for i in range(len(from_list)):
_ = max_flow.AddArcWithCapacity(
from_list[i], to_list[i], caps[i]
)
# 3- 最大流-求解
if max_flow.Solve(start_node_idx, end_node_idx) == max_flow.OPTIMAL:
max_flow_res = max_flow.OptimalFlow()
print('Max flow:', max_flow_res)
else:
return None
# 4- 初始化最小费用优化器
min_cost = pywrapgraph.SimpleMinCostFlow()
# 5- 最小费用图构建
for i in range(len(fList)):
_ = min_cost.AddArcWithCapacityAndUnitCost(
from_list[i], to_list[i], caps[i], costs[i]
)
supplies = [0] * len(from_list)
supplies[start_node_idx] = max_flow_res
supplies[end_node_idx] = -max_flow_res
for i in range(len(supplies)):
_ = min_cost.SetNodeSupply(i, supplies[i])
res_path = []
# 6- 求解
if min_cost.Solve() == min_cost.OPTIMAL:
min_cost_res = min_cost.OptimalCost()
print('Minimum cost:', min_cost_res)
print('')
print(' Arc Flow / Capacity Cost')
for i in range(min_cost.NumArcs()):
cost = min_cost.Flow(i) * min_cost.UnitCost(i)
if min_cost.Flow(i) > 0 and cost > 0:
res_path.append("%s->%s" % (min_cost.Tail(i), min_cost.Head(i)) )
print('%1s -> %1s %3s / %3s %3s' % (
min_cost.Tail(i),
min_cost.Head(i),
min_cost.Flow(i),
min_cost.Capacity(i),
cost))
else:
return
return {"flowPath": res_path, 'maxFlow': max_flow_res, "minCost": min_cost_res}
fList = [0, 0, 1, 1, 1, 2, 2, 3, 4]
toList = [1, 2, 2, 3, 4, 3, 4, 4, 2]
cost = [4, 4, 2, 2, 6, 1, 3, 2, 3]
cap = [15, 8, 20, 4, 10, 15, 4, 20, 5]
ortoolsMCMF(fList, toList, cap, cost, 0, 4)