网约车在人们的日常出行生活中扮演了十分重要的角色,它为出租车和乘客搭建了一个方便的业务平台,既减少了乘客的候车时间,又很大程度上缓解了出租车的空载现象,从而大大提高了人们的出行效率。网约车的订单分配模式一般包括抢单模式和系统派单模式,这里实现了系统派单模式下的订单分配策略,即保证所有乘客等待的时间全局最小。使用Dijkstra算法来计算每辆出租车到每个乘客的最短路径,并通过KM算法求得出租车和乘客之间的最优匹配,来保证所有出租车到乘客之间所需的行驶距离最短,从而保证乘客所需的候车时间也全局最短。
左边图是对实际的网约车订单分配问题的一个简化模型,右边是对模型的抽象。在加权关系图中,我们将每一个交叉路口视为在图中的一个结点,结点之间的权重表示两个路口之间的距离,而出租车和乘客都位于某一个路口,并可以被视为图中的一个结点。
将关系图转换为加权邻接矩阵作为程序的输入,并在程序的开始初始化出租车和乘客在图中的位置。在使用Dijkstra算法求出租车到乘客的最短路径时,因为我们求的是有限点的最短路径,所以我们将遍历了所有要求结点(在这个问题中是乘客结点)作为结束Dijkstra算法的条件以减少运算量。因为我们需要求的是订单的全局最优匹配,即最小化所有出租车到对应乘客所花费的时间之和最小。而在原始的KM算法中是以最大化最优匹配作为目标的,所以在处理时需要将对应的权重取负来保证得到正确的结果。
代码实现
# -*- coding:utf-8 -*-
# @Time : 2021/11/13 15:26
# @Author: zhcode
# @File : taxi_dispatcher.py
import sys
import copy
import json
"""
1. 使用dijkstra发现出租车到乘客的最短距离
2. 使用二部图最优匹配,将出租车和乘客进行匹配
"""
path = './graph.json'
def json_traverse(data: dict):
"""
遍历字典,并返回ndarray格式的元组
:param data: 要遍历的地图数据
:return: 图的邻接矩阵
"""
graph_items = [(item[0], item[1]) for item in data[0].items()]
point_num = len(graph_items)
adjacency_matrix = [[float('inf') for j in range(point_num)] for i in range(point_num)]
for k, v in data[0].items():
row_index = ord(k) - ord('A')
adjacency_matrix[row_index][row_index] = 0
for edge, dis in v.items():
column_index = ord(edge) - ord('A')
adjacency_matrix[row_index][column_index] = dis
adjacency_matrix[column_index][row_index] = dis
# print(k, v)
return adjacency_matrix
def dijkstra(graph_matrix, src):
"""
实现迪杰斯特拉算法
:param graph_matrix: 图的邻接矩阵
:param src: 源点
:param des: 终点,当算法访问到终点时,停止算法
:return: 返回最短的距离和路径
"""
graph = None
if graph_matrix == None:
return None
else:
graph = graph_matrix
# 定点集合
nodes = [i for i in range(len(graph))] # 获取顶点列表,用邻接矩阵存储图
# 顶点是否被访问
visited = []
# 将源点加入已访问节点
# visited.append(src)
# 初始化dis
dis = {src: 0} # 源点到自身的距离为0
# 初始化距离
for i in nodes:
dis[i] = graph[src][i]
path = {src: {src: []}} # 记录源节点到每个节点的路径
k = pre = src
while nodes:
temp_k = k
mid_distance = float('inf') # 设置中间距离无穷大
for v in visited:
for d in nodes:
# A 到 Z, Z 再到 Q
if graph[src][v] != float('inf') and graph[v][d] != float('inf'): # 有边
new_distance = graph[src][v] + graph[v][d]
if new_distance <= mid_distance:
mid_distance = new_distance
graph[src][d] = new_distance # 进行距离更新
k = d
pre = v
# 当已经遍历了所有的节点, 就终止算法
if k != src and not nodes:
break
dis[k] = mid_distance # 最短路径
# 保存当前最短路径
path[src][k] = [i for i in path[src][pre]]
path[src][k].append(k)
if k not in visited:
visited.append(k)
nodes.remove(k)
return dis, path
def search_batch(u: int) -> bool:
"""
给x[u]找匹配,这个过程和匈牙利匹配是一样的
:param u:
:return:
"""
global weight, n, Cx, Cy, sx, sy, match
sx[u] = True
for v in range(n):
if not sy[v] and Cx[u] + Cy[v] == weight[u][v]:
sy[v] = True
if match[v] == -1 or search_batch(match[v]):
match[v] = u
return True
return False
def kuhn_munkras(max_weight: bool) -> list:
"""
实现km算法
:param max_weight:
:return:
"""
global weight, n, Cx, Cy, sx, sy, match
negate_weight = lambda x: -x
if not max_weight:
# 对权重进行取反操作,因为是按照出租车到乘客的最短距离进行匹配
weight = [[negate_weight(weight[i][j]) for j in range(n)] for i in range(n)]
for u in range(n):
# 不断修改顶标,直到完美匹配
while True:
sx = [0 for i in range(n)]
sy = [0 for i in range(n)]
if search_batch(u):
break
inc = sys.maxsize
for j in range(n):
if sx[j]:
for k in range(n):
if not sy[k] and (Cx[j] + Cy[k] - weight[j][k]) < inc:
inc = Cx[j] + Cy[k] - weight[j][k]
for j in range(n):
if sx[j]:
Cx[j] -= inc
if sy[j]:
Cy[j] += inc
return [match[i] for i in range(n) if match[i] >= 0]
if __name__ == '__main__':
# 出租车的位置
taxi_pos = [0, 10, 18]
# 乘客的位置
passenger_pos = [5, 7, 24]
# 保存各个出租车到各个乘客的路径信息
path_infos = [[{} for j in range(len(passenger_pos))] for i in range(len(taxi_pos))]
# 输入的有向图,有边存储的就是边的权值,无边就是float('inf'),顶点到自身就是0
graph = None
with open(path, 'r', encoding='utf-8') as f:
graph = json.load(f)
graph = json_traverse(graph)
for i, taxi in enumerate(taxi_pos):
graph_copy = copy.deepcopy(graph)
dis, path = dijkstra(graph_copy, taxi) # 查找从源点0开始带其他节点的最短路径
for from_point, to_data in path.items():
for k, vals in to_data.items():
if k in passenger_pos:
drive_path = [chr(ord('A') + val) for val in vals]
path_infos[i][passenger_pos.index(k)]['d'] = dis[k]
path_infos[i][passenger_pos.index(k)]['p'] = drive_path
# 输出路径信息
# print("{}--->{}:".format(chr(ord('A') + from_point), chr(ord('A') + k)), end=" ")
# print(drive_path, end=" ")
# for item in path_infos:
# print(item)
weight = [[val['d'] for val in item] for item in path_infos]
n = len(weight)
# x 的定点标号,为与之相连的边的最大权值
Cx = [max(row) for row in weight]
# y 的定点标号初始化为0
Cy = [0 for i in range(n)]
# 记录寻找增广路时,点集x,y里的点是否搜索过
sx, sy = [], []
# 初始化match数组为未匹配
match = [-1 for i in range(n)]
# for match_i in kuhn_munkras(0):
total_dis = 0
for i, match_passenger in enumerate(kuhn_munkras(0)):
print("出租车{}分配乘客{}的订单, 距离为{}".format(chr(ord('A') + taxi_pos[i]), chr(ord('A') + passenger_pos[match_passenger]), path_infos[i][match_passenger]['d'], end=" "))
print("规划路径为:", end=" ")
for point in path_infos[i][match_passenger]['p']:
print(point, end=" ")
print("\n", "-"*25)
作为输入的文件 graph.json
程序运行结果