1. 需求
想要使用Python实现一个出租车仿真环境,其中每个时间窗口内产生的request及其周围的taxi满足一个二分图的关系。原本计划request与taxi之间的匹配按照接客时间权值最小为目标进行匹配,但是后面实践的时候觉得存在一定问题:①单个目标的权值设置为订单出行距离/接客时间比仅用接客时间作为权值更好。②最小匹配+非完备匹配+存在不可连接点的约束使得出现很多bug,且没想到解决方案。于是后面觉得改用出行距离/接客时间作为权值实现二分图最优匹配。
2.KM算法
KM算法是解决带权值的二分匹配问题的经典算法。解决request和taxi的匹配问题首先需要先了解二分图以及二分图的最优匹配(KM算法)。
二分图的理解这里不做解释,仅提示区分好二分图的几个重要概念:
- 完备匹配:X或Y中全部匹配完
- 完美匹配:XY都匹配完
- 最佳匹配:权值最大的完备匹配(不是绝对的权值最大,因为限定了完备)。但可以增加一些权值为0的边使其统一起来
- 带权匹配:就是求出一个匹配集合,使得集合中边的权值之和最大或最小
- 相等子图:选择‘边权=顶标和’的边组成
- 匈牙利算法是利用了增广的性质,使整个图匹配数最大。KM算法利用匈牙利算法求完备匹配下的带权二分图最佳匹配的算法。
KM算法理解可参考:
重要的操作和踩坑(提醒):
- 最小匹配的问题可以通过权值取反实现(当权值都为正数时)
- KM算法必须存在一个完备匹配,如果求一个最大权匹配(不一定完备),添加一些边权值为0的连接,实现最大权匹配。
- KM算法求是边权值和最大,若需边权之积最大,对每条边权取自然对数,然后求最大和权匹配,求得的结果a再算出e^a可得最大积匹配。
- 算法中每次是选择交错树中顶标和与边权差最小的边。(计算一个d=min{L(x) + L(y) - wight(x,y)}
- 设计算法按照不同的需求可能多次出现使用一个很大的数,如①代表二分图不可连接两点之间权值、②初始化的最小已寻找到的最小连接inc,③用大数-原权值求最小匹配。 的过程中注意区分几个很大的数之间的大小关系
KM算法实现
可以直接调用的函数
# 求的是最小匹配,idexes 返回所得连线的列表(元祖)
m = Munkres()
indexes = m.compute(matrix)
使用方法:
"""
@author:HY
@time:2020/12/3:19:15
"""
# 两个可以单独使用的函数
def cost(graph):
"""
计算成本函数(路径最小化)
:return:
"""
# graph = [[5, 9, 1],
# [10, 3, 2],
# [8, 7, 4]]
m = Munkres()
indexes = m.compute(graph) # idexes 返回的是连线的列表(元祖)
print(indexes)
total = 0
for row, column in indexes:
value = graph[row][column]
total += value
print('最小成本%r:'%total)
def profit():
"""
计算利润的函数(路径最大化)
其实利用一个取相反数转化为成本的形式再计算的,得到对应的线最后再从原本的矩阵中取值。也可以使用大数-原权值的方法
:return:
"""
graph = [[5, 9, 1],
[10, 3, 2],
[8, 7, 4]]
cost_graph = []
for row in graph:
cost_row = []
for col in row:
cost_row += [- col] # 这里课改为 大数-col 的形式
cost_graph += [cost_row]
m = Munkres()
indexes = m.compute(cost_graph)
total = 0
for row, column in indexes:
value = graph[row][column]
total += value
print('最大利润%r:'%total)
内部逻辑完整实现
按照对算法的理解和部分资料的改编结合。按照左边的男生和右边的女生为二分图匹配案例设计,故代码中的left和boy是相等概念,right和girl是相等概念。
"""
@author:HY
@time:2020/12/3:19:15
"""
import numpy as np
from munkres import Munkres, print_matrix
import sys
class KM_Algorithm_1:
"""
此类是一个对KM算法的成功实现。
本需求中不可直接使用,因为订单与车辆之间可能不能匹配的边以及订单会匹配失败的可能性
"""
def __init__(self, Bipartite_Graph):
self.Bipartite_Graph = Bipartite_Graph
# 左右结点数量记录
self.left = self.Bipartite_Graph.shape[0] # 以左边为主
self.right = self.Bipartite_Graph.shape[1]
# 初始化顶标
self.label_left = np.max(self.Bipartite_Graph, axis=1) # 设置左边顶标为权重最大值(每行的最大值)
self.label_right = np.zeros(self.right) # 右边集合的顶标设置为0
# 初始化辅助变量——是否已匹配
self.visit_left = np.zeros(self.left, dtype=bool)
self.visit_right = np.zeros(self.right, dtype=bool)
# 初始化右边的匹配结果.如果已匹配就会对应匹配结果
self.match_right = np.empty(self.left