【发布时间】:2021-07-22 09:34:56
【问题描述】:
问题设置
目前,我们正在为一家食品科技初创公司(电子杂货店)解决配送问题。我们有工作(要交付的订单)和工人(快递员/包装员/通用)问题是如何有效地将订单分配给工人。第一步,我们决定优化 CTE(点击即食 - 下单和送餐之间的时间)
问题本身
问题来自这样一个事实,即有时每个作业有 2 个工人而不是单个执行器是有效的,因为打包员可能知道商店“地图”,而快递员可能有自行车,与每个作业相比,它可以更快地执行作业它们甚至分别计算订单转移成本。
我们研究了算法,发现我们的问题看起来像assignment problem,并且有一个算法解决方案(Hungarian algorithm),但问题是经典问题要求“每个工作分配给一个工人,每个工人分配一份工作”,而在我们的例子中,一份工作有 2 名工人有时是有效的。
到目前为止我们所做的尝试
-
插入 (packer A + universal B) 组合到成本矩阵中,但在这种情况下,我们不能将 universal B 添加到成本矩阵中矩阵,因为结果我们可以得到 universal B 将被分配给 2 个工作(作为一个单独的单元并作为与 packer A 组合的一部分)
-
因此实施 2 种匈牙利算法:首先分配包装,然后分配交付。它在绝大多数情况下都有效,但有时会导致效率低下的解决方案。如果需要,我会添加一个示例。
问题本身
我用谷歌搜索了很多,但找不到任何可以指导我解决问题的方法。如果您有任何链接或想法可供我们用作解决问题的线索,我将很乐意检查它们。
编辑:我添加了我的问题的蛮力解决方案。希望这有助于更好地理解问题
# constants
delivery_speed = np.array([5, 13]) # km per hour
delivery_distance = np.array([300, 2700]) # meters
flight_distance = np.array([500, 1900]) # meters время подлета
positions_amount = np.array([4, 8]) # number of positions in one order
assembly_speed = np.array([2, 3]) # minutes per position
transit_time = 5 * 60 # sec to transfer order
number_of_orders = 3 # number of orders in a batch
number_of_workers = 3
# size of optimization matrix
matrix_size = max(number_of_workers, number_of_orders)
# maximum diagonal length for delivery and flight
max_length = np.sqrt(max(delivery_distance)**2/2)
max_flight_length = np.sqrt(max(flight_distance)**2/2)
# store positions
A = np.array([delivery_distance[1], delivery_distance[1]])
B = np.array([A[0] + max_length / 2, A[1]])
possible_order_position_x = np.array([-max_length/2, max_length]) + A[0]
possible_order_position_y = np.array([-max_length, max_length]) + A[1]
possible_courier_position_x = np.array([-max_flight_length/2, max_flight_length]) + A[0]
possible_courier_position_y = np.array([-max_flight_length, max_flight_length]) + A[1]
# generate random data
def random_speed(speed_array):
return np.random.randint(speed_array[0], speed_array[1]+1)
def location(possible_x, possible_y):
return np.random.randint([possible_x[0], possible_y[0]],
[possible_x[1], possible_y[1]],
size=2)
def generate_couriers():
# generate couriers
couriers = {}
for courier in range(number_of_workers):
couriers[courier] = {
'position': location(possible_courier_position_x, possible_courier_position_y),
'delivery_speed': random_speed(delivery_speed),
'assembly_speed': random_speed(assembly_speed),
}
return couriers
couriers = generate_couriers()
store_location = {0: A, 1:B}
def generate_orders():
# generate orders
orders = {}
for order in range(number_of_orders):
orders[order] = {
'number_of_positions': random_speed(positions_amount),
'store_position': store_location[np.random.randint(2)],
'customer_position': location(possible_order_position_x, possible_order_position_y)
}
return orders
orders = generate_orders()
orders
# functions to calculate assembly and delivery speed
def travel_time(location_1, location_2, speed):
# time to get from current location to store
flight_distance = np.linalg.norm(location_1 - location_2)
delivery_speed = 1000 / (60 * 60) * speed # meters per second
return flight_distance / delivery_speed # seconds
def assembly_time(courier, order):
flight_time = travel_time(courier['position'], order['store_position'], courier['delivery_speed'])
assembly_time = courier['assembly_speed'] * order['number_of_positions'] * 60
return int(flight_time + assembly_time)
assembly_time(couriers[0], orders[0])
def brute_force_solution():
best_cte = np.inf
best_combination = [[],[]]
for first_phase in itertools.permutations(range(number_of_workers), number_of_orders):
assembly_time_s = pd.Series(index = range(number_of_orders), dtype=float)
for order, courier in enumerate(first_phase):
assembly_time_s[order] = assembly_time(couriers[courier], orders[order])
# start to work with delivery
for second_phase in itertools.permutations(range(number_of_workers), number_of_orders):
delivery_time_s = pd.Series(index = range(number_of_orders), dtype=float)
for order, courier in enumerate(second_phase):
delivery_time = travel_time(orders[order]['store_position'],
orders[order]['customer_position'],
couriers[courier]['delivery_speed'])
# different cases for different deliveries
if courier == first_phase[order]:
# if courier assemblied order, then deliver immidietely
delivery_time_s[order] = delivery_time
elif courier not in first_phase:
# flight during assembly, wait if needed, transfer time, delivery
flight_time = travel_time(orders[order]['store_position'],
couriers[courier]['position'],
couriers[courier]['delivery_speed'])
wait_time = max(flight_time - assembly_time_s[order], 0)
delivery_time_s[order] = transit_time + wait_time + delivery_time
else:
# case when shopper transfers her order and moves deliver other
# check if second order is in the same store
first_phase_order = first_phase.index(courier)
if (orders[first_phase_order]['store_position'] == orders[order]['store_position']).all():
# transit time - fixed and happens only once!
# wait, if second order has not been assemblied yet
# time to assembly assigned order
assembly_own = assembly_time_s[first_phase_order]
# time to wait, if order to deliver is assemblied slower
wait_time = max(assembly_time_s[order] - assembly_own, 0)
# delivery time is calculated as loop start
delivery_time_s[order] = transit_time + wait_time + delivery_time
else:
# transit own order - flight to the other store - wait if needed - tansit order - delivery_time
flight_time = travel_time(orders[first_phase_order]['store_position'],
orders[order]['store_position'],
couriers[courier]['delivery_speed'])
arrival_own = (assembly_time_s[first_phase_order] + transit_time + flight_time)
wait_time = max(assembly_time_s[order] - arrival_own, 0)
delivery_time_s[order] = ((transit_time * 2) + flight_time + wait_time + delivery_time)
delivery_time_s = delivery_time_s.astype(int)
# calculate and update best result, if needed
cte = (assembly_time_s + delivery_time_s).sum()
if cte < best_cte:
best_cte = cte
best_combination = [list(first_phase), list(second_phase)]
return best_cte, best_combination
best_cte, best_combination = brute_force_solution()
【问题讨论】:
-
为什么不把每个工作分成两个任务(打包任务和快递任务)并运行一次匈牙利算法?
-
@kaya3 你能解释一下如何运行一次吗?我们不能这样做,因为我们可以让工人 1 做工作 1,工人 2 做工作 1,工人 1 + 工人 2 做工作 1,工人 1 + 工人 3 做工作 2。因此我们得到一些任务将无法满足结合
-
您需要最优解,还是启发式就足够了?
-
@RBarryYoung 起初我们会对启发式算法感到满意,但一旦我们的销售额增长,我们将希望找到最佳解决方案并以足够快的速度进行生产
-
这不再是分配问题。我会开始把它写成一个正式的优化问题(并作为一个 MIP/CP 模型来解决)。这为实验提供了一个很好的工具(它是否工作、评估解决方案、评估性能)。如果需要,以后可以开发一种启发式的性能。使用优化模型,您至少可以与最佳解决方案进行比较(否则您将完全处于解决方案质量的黑暗中)。如果你没有这方面的经验,也许可以聘请一名学生来帮助解决这个问题。
标签: algorithm linear-programming hungarian-algorithm assignment-problem