数据结构_图优化-最小费用最大流MCMF(python解)

一、原理

线性规划问题有一个有趣的特性,即所有求极大的问题都有一个与其匹配的求极小的线性规划问题。我们通过求解一个问题的对偶问题,再加以转化就可以得到原始问题的解

1.1 最基本的思想

  • 这里的原始对偶算法是一种最小费用路算法,因而它最基本的思想便是贪心
  • 我们保证程序运行的任意时刻,当前流量为r,那么花费时流量r时最小的费用。这样不断增大流量直至最大。就得到了最小费用最大流

1.2 SPFA实现伪代码

  1. 创建具有反向边的图G(V, E)
  2. 设置起终点
  3. 通过SPFA(shortest Path Fast Algorithim)寻找cost最小的一个路径
    a. 最终返回终点的cost是否是小于最大值。
  4. 更新图
    a. 寻找路径上的最小流量minFlow
    b. 将路径上的流量均减去 minFlow, 同时对应反向边加上minFlow
  5. 循环3和4 直到3返回为False
  6. 将每次的路径minFlow相加就是最终的maxFlow, 将每次的路径的费用相加即为最小费用。

二、实现

2.1 python实现

算法核心:

  • 图结构G(V, E)
    • GVector 内部类-节点
    • Edge 内部类-边 保存 from, to, cap, cost, antiIndex(反向边在所在节点位置)
    • Graph: {vIdex: List[Edge]} 邻接表 vIdex和v_list相对应
    • v_list: List[GVector] 节点列表
  • spfa:沿着允许弧进行扩增,允许边 cap > 0 and cost_list[t] > cost_list[now] + cst
    • 辅助空间:
    • visted_list: 记录节点是否被访问,防止死循环,以及有效进行扩增
    • cost_list: 记录起点到当前节点路径的累积cost
    • pre_vector: 路径节点记录,pre_vector[now]:表示为到达now节点的上个节点
    • pre_vector_edge:路径边记录,pre_vector_edge[now]:表示为通过该边到达now节点
  • solve: 寻找路径最小流量,更新边的权重(注意对应边流量-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
}

2.2 使用ortools实现

ortools 具两个优化器

  • SimpleMaxFlow 最大流
  • SimpleMinCostFlow 流对应的最小费用

只需要将两者以线性相结合就可以得到我们的最小费用最大流(MCMF)。

maxFlow 与 minCost maxFlow 比对

最小费用最大流的一般求解方法

  • 通过增广路径,扩增允许边寻找最小费用的路径 (SPFA-扩增同样也用BFS,只是约束不一样,具有唯一性),若无,则结束
  • 然后用边上的流量来更新图
  • 寻找到没有路径为止

解的边集是唯一的

最大流的求一般解方法

  • 在寻找增广路径时,可以用BFS来找,并且更新残留网络的值(涉及到反向边)
  • 反复寻找源点s到汇点t之间的增广路径,若有,找出增广路径上每一段[容量-流量]的最小值delta(BFS等),若无,则结束。
  • 用边上的流量来更新图
  • 寻找到没有路径为止

解的边集不是唯一的

所以,只要限制寻找的路径流量,寻找最小费用,那么也可以得到最小费用最大流
只要限制的值是最大流(流量是唯一的,边集合不一定唯一),去寻找最小费用的边集
那么最终结果也将是一致。即组合SimpleMaxFlowSimpleMinCostFlow 就可得到最终的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)

你可能感兴趣的:(数据结构,python,数据结构,算法,运筹优化)