每个工作有 2 个工人的分配问题
Posted
技术标签:
【中文标题】每个工作有 2 个工人的分配问题【英文标题】:Assignment problem with 2 workers per job 【发布时间】: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 模型来解决)。这为实验提供了一个很好的工具(它是否工作、评估解决方案、评估性能)。如果需要,以后可以开发一种启发式的性能。使用优化模型,您至少可以与最佳解决方案进行比较(否则您将完全处于解决方案质量的黑暗中)。如果你没有这方面的经验,也许可以聘请一名学生来帮助解决这个问题。 【参考方案1】:匈牙利算法是一种过时的解决方案,由于某些我不明白的原因(也许是因为它在概念上很简单)仍然很受欢迎。
使用最小成本流来模拟您的问题。它更加灵活,并且有许多有效的算法。也可以证明它可以解决匈牙利算法可以解决的任何问题(证明很简单。)
鉴于您对问题的描述非常模糊,您可能希望使用两层节点 V = (O,W) 对底层图 G=(V,E) 建模,其中 O 是订单,W 是工人.
边可以是定向的,每个 Worker 对每个可能的订单都有一个容量为 1 的边。将源节点连接到边缘容量为 1 的工作节点,并将每个订单节点连接到容量为 2(或更高,每个订单允许更多工作人员)的汇节点。
我上面描述的实际上是一个 maxflow 实例而不是 MCF,因为它没有分配权重。但是,您可以为任何边分配权重。
鉴于您的问题表述,我不明白这甚至是一个分配问题,您是否可以不使用简单的先到先分配(队列)类型策略,因为您似乎没有任何标准来拥有工人更喜欢按某个订单工作而不是另一个订单。
【讨论】:
抱歉,但我显然有标准,我需要优化点击即食 - 所以通常离工作最近的工人是最可取的。然而,有时在距离工作很远的地方骑自行车的人可能比步行快递员更有效率。在我的送货服务中,我们每分钟收到 X 个订单,我们应该将我们的工人分配给这些订单,这实际上是一个分配问题,我相信 所以我们的成本是飞行时间(到达商店的时间)+ 购买所需订单的物品 + 交付。有时我们将 2 名工人分配给一个订单,因为由一名工人购买物品并由另一名工人交付可能是有效的 好的,您对问题的设置方式非常含糊。如果我理解正确,可以为工人分配自行车司机或采购员吗?我们可以说效率(收入减去成本)是购买者的 X 和购买者和自行车司机的 X + Y(即效果是线性的)吗?如果效果是非线性的,你的问题会更难。【参考方案2】:我使用可以处理团队的模型进行了快速而简单的测试。仅用于说明目的。
我创建了两种类型的工人:A 型和 B 型,以及由两名工人组成的团队(每种类型一个)。此外,我还创建了随机成本数据。
这是所有数据的部分打印输出。
---- 13 SET i workers,teams
a1 , a2 , a3 , a4 , a5 , a6 , a7 , a8 , a9 , a10
b1 , b2 , b3 , b4 , b5 , b6 , b7 , b8 , b9 , b10
team1 , team2 , team3 , team4 , team5 , team6 , team7 , team8 , team9 , team10
team11 , team12 , team13 , team14 , team15 , team16 , team17 , team18 , team19 , team20
team21 , team22 , team23 , team24 , team25 , team26 , team27 , team28 , team29 , team30
team31 , team32 , team33 , team34 , team35 , team36 , team37 , team38 , team39 , team40
team41 , team42 , team43 , team44 , team45 , team46 , team47 , team48 , team49 , team50
team51 , team52 , team53 , team54 , team55 , team56 , team57 , team58 , team59 , team60
team61 , team62 , team63 , team64 , team65 , team66 , team67 , team68 , team69 , team70
team71 , team72 , team73 , team74 , team75 , team76 , team77 , team78 , team79 , team80
team81 , team82 , team83 , team84 , team85 , team86 , team87 , team88 , team89 , team90
team91 , team92 , team93 , team94 , team95 , team96 , team97 , team98 , team99 , team100
---- 13 SET w workers
a1 , a2 , a3 , a4 , a5 , a6 , a7 , a8 , a9 , a10, b1 , b2 , b3 , b4 , b5
b6 , b7 , b8 , b9 , b10
---- 13 SET a a workers
a1 , a2 , a3 , a4 , a5 , a6 , a7 , a8 , a9 , a10
---- 13 SET b b workers
b1 , b2 , b3 , b4 , b5 , b6 , b7 , b8 , b9 , b10
---- 13 SET t teams
team1 , team2 , team3 , team4 , team5 , team6 , team7 , team8 , team9 , team10
team11 , team12 , team13 , team14 , team15 , team16 , team17 , team18 , team19 , team20
team21 , team22 , team23 , team24 , team25 , team26 , team27 , team28 , team29 , team30
team31 , team32 , team33 , team34 , team35 , team36 , team37 , team38 , team39 , team40
team41 , team42 , team43 , team44 , team45 , team46 , team47 , team48 , team49 , team50
team51 , team52 , team53 , team54 , team55 , team56 , team57 , team58 , team59 , team60
team61 , team62 , team63 , team64 , team65 , team66 , team67 , team68 , team69 , team70
team71 , team72 , team73 , team74 , team75 , team76 , team77 , team78 , team79 , team80
team81 , team82 , team83 , team84 , team85 , team86 , team87 , team88 , team89 , team90
team91 , team92 , team93 , team94 , team95 , team96 , team97 , team98 , team99 , team100
---- 13 SET j jobs
job1 , job2 , job3 , job4 , job5 , job6 , job7 , job8 , job9 , job10, job11, job12
job13, job14, job15
---- 23 SET team composition of teams
team1 .a1 , team1 .b1 , team2 .a1 , team2 .b2 , team3 .a1 , team3 .b3 , team4 .a1
team4 .b4 , team5 .a1 , team5 .b5 , team6 .a1 , team6 .b6 , team7 .a1 , team7 .b7
team8 .a1 , team8 .b8 , team9 .a1 , team9 .b9 , team10 .a1 , team10 .b10, team11 .a2
team11 .b1 , team12 .a2 , team12 .b2 , team13 .a2 , team13 .b3 , team14 .a2 , team14 .b4
team15 .a2 , team15 .b5 , team16 .a2 , team16 .b6 , team17 .a2 , team17 .b7 , team18 .a2
team18 .b8 , team19 .a2 , team19 .b9 , team20 .a2 , team20 .b10, team21 .a3 , team21 .b1
team22 .a3 , team22 .b2 , team23 .a3 , team23 .b3 , team24 .a3 , team24 .b4 , team25 .a3
team25 .b5 , team26 .a3 , team26 .b6 , team27 .a3 , team27 .b7 , team28 .a3 , team28 .b8
team29 .a3 , team29 .b9 , team30 .a3 , team30 .b10, team31 .a4 , team31 .b1 , team32 .a4
team32 .b2 , team33 .a4 , team33 .b3 , team34 .a4 , team34 .b4 , team35 .a4 , team35 .b5
team36 .a4 , team36 .b6 , team37 .a4 , team37 .b7 , team38 .a4 , team38 .b8 , team39 .a4
team39 .b9 , team40 .a4 , team40 .b10, team41 .a5 , team41 .b1 , team42 .a5 , team42 .b2
team43 .a5 , team43 .b3 , team44 .a5 , team44 .b4 , team45 .a5 , team45 .b5 , team46 .a5
team46 .b6 , team47 .a5 , team47 .b7 , team48 .a5 , team48 .b8 , team49 .a5 , team49 .b9
team50 .a5 , team50 .b10, team51 .a6 , team51 .b1 , team52 .a6 , team52 .b2 , team53 .a6
team53 .b3 , team54 .a6 , team54 .b4 , team55 .a6 , team55 .b5 , team56 .a6 , team56 .b6
team57 .a6 , team57 .b7 , team58 .a6 , team58 .b8 , team59 .a6 , team59 .b9 , team60 .a6
team60 .b10, team61 .a7 , team61 .b1 , team62 .a7 , team62 .b2 , team63 .a7 , team63 .b3
team64 .a7 , team64 .b4 , team65 .a7 , team65 .b5 , team66 .a7 , team66 .b6 , team67 .a7
team67 .b7 , team68 .a7 , team68 .b8 , team69 .a7 , team69 .b9 , team70 .a7 , team70 .b10
team71 .a8 , team71 .b1 , team72 .a8 , team72 .b2 , team73 .a8 , team73 .b3 , team74 .a8
team74 .b4 , team75 .a8 , team75 .b5 , team76 .a8 , team76 .b6 , team77 .a8 , team77 .b7
team78 .a8 , team78 .b8 , team79 .a8 , team79 .b9 , team80 .a8 , team80 .b10, team81 .a9
team81 .b1 , team82 .a9 , team82 .b2 , team83 .a9 , team83 .b3 , team84 .a9 , team84 .b4
team85 .a9 , team85 .b5 , team86 .a9 , team86 .b6 , team87 .a9 , team87 .b7 , team88 .a9
team88 .b8 , team89 .a9 , team89 .b9 , team90 .a9 , team90 .b10, team91 .a10, team91 .b1
team92 .a10, team92 .b2 , team93 .a10, team93 .b3 , team94 .a10, team94 .b4 , team95 .a10
team95 .b5 , team96 .a10, team96 .b6 , team97 .a10, team97 .b7 , team98 .a10, team98 .b8
team99 .a10, team99 .b9 , team100.a10, team100.b10
---- 28 PARAMETER c cost coefficients
job1 job2 job3 job4 job5 job6 job7 job8 job9
a1 17.175 84.327 55.038 30.114 29.221 22.405 34.983 85.627 6.711
a2 63.972 15.952 25.008 66.893 43.536 35.970 35.144 13.149 15.010
a3 11.049 50.238 16.017 87.246 26.511 28.581 59.396 72.272 62.825
a4 18.210 64.573 56.075 76.996 29.781 66.111 75.582 62.745 28.386
a5 7.277 17.566 52.563 75.021 17.812 3.414 58.513 62.123 38.936
a6 78.340 30.003 12.548 74.887 6.923 20.202 0.507 26.961 49.985
a7 99.360 36.990 37.289 77.198 39.668 91.310 11.958 73.548 5.542
a8 22.575 39.612 27.601 15.237 93.632 42.266 13.466 38.606 37.463
a9 10.169 38.389 32.409 19.213 11.237 59.656 51.145 4.507 78.310
a10 50.659 15.925 65.689 52.388 12.440 98.672 22.812 67.565 77.678
b1 73.497 8.544 15.035 43.419 18.694 69.269 76.297 15.481 38.938
b2 8.712 54.040 12.686 73.400 11.323 48.835 79.560 49.205 53.356
b3 2.463 17.782 6.132 1.664 83.565 60.166 2.702 19.609 95.071
b4 39.334 80.546 54.099 39.072 55.782 93.276 34.877 0.829 94.884
b5 58.032 16.642 64.336 34.431 91.233 90.006 1.626 36.863 66.438
b6 49.662 4.493 77.370 53.297 74.677 72.005 63.160 11.492 97.116
b7 79.070 61.022 5.431 48.518 5.255 69.858 19.478 22.603 81.364
b8 81.953 86.041 21.269 45.679 3.836 32.300 43.988 31.533 13.477
b9 6.441 41.460 34.161 46.829 64.267 64.358 33.761 10.082 90.583
b10 40.419 11.167 75.113 80.340 2.366 48.088 27.859 90.161 1.759
team1 50.421 83.126 60.214 8.225 57.776 59.318 68.377 15.877 33.178
team2 57.624 71.983 68.373 1.985 83.980 71.005 15.551 61.071 66.155
team3 1.252 1.017 95.203 97.668 96.632 85.628 14.161 4.973 55.303
team4 34.968 11.734 58.598 44.553 41.232 91.451 21.378 22.417 54.233
team5 31.014 4.020 82.117 23.096 41.003 30.258 44.492 71.600 59.315
team6 68.639 67.463 33.213 75.994 17.678 68.248 67.299 83.121 51.517
team7 8.469 57.216 2.206 74.204 90.510 56.082 47.283 71.756 51.301
team8 96.552 95.789 89.923 32.755 45.710 59.618 87.862 17.067 63.360
team9 33.626 58.864 57.439 54.342 57.816 97.722 32.147 76.297 96.251
. . .
team98 21.277 8.252 28.341 97.284 47.146 22.196 56.537 89.966 15.708
team99 77.385 12.015 78.861 79.375 83.146 11.379 3.413 72.925 88.689
team100 11.050 20.276 21.448 27.928 15.073 76.671 91.574 94.498 7.094
(cost data for other jobs skipped)
我尝试将此建模为混合整数编程模型如下:
显然,这不再是一个纯粹的分配问题。第一个约束比我们习惯的要复杂。它说对于每个工人 w,我们可以分配他/她自己或任何有 w 作为成员的团队只有一次。
当我在不使用团队的情况下解决此问题时,我得到以下解决方案:
---- 56 VARIABLE x.L assignment
job1 job2 job3 job4 job5 job6 job7 job8 job9
a5 1
a6 1
b1 1
b3 1
b4 1
b6 1
b8 1
b9 1
b10 1
+ job10 job11 job12 job13 job14 job15
a1 1
a4 1
a7 1
b2 1
b5 1
b7 1
---- 56 VARIABLE z.L = 59.379 total cost
这是一个标准的分配问题,但我作为 LP 解决了它(所以我可以使用相同的工具)。
当我允许团队时,我得到:
---- 65 VARIABLE x.L assignment
job1 job2 job3 job4 job5 job6 job7 job8 job9
a1 1
a5 1
a6 1
b3 1
b4 1
b9 1
b10 1
team17 1
team28 1
+ job10 job11 job12 job13 job14 job15
a4 1
a7 1
b2 1
b5 1
team86 1
team91 1
---- 65 VARIABLE z.L = 40.057 total cost
目标更好(只是因为它可以选择“成本”低的团队)。另外,请注意,在解决方案中,没有选择两次工作人员(单独或团队的一部分)。这里有一些额外的解决方案报告可以证实这一点:
---- 70 SET sol alternative solution report
job1 job2 job3 job4 job5 job6 job7 job8 job9
team17.a2 YES
team17.b7 YES
team28.a3 YES
team28.b8 YES
- .a1 YES
- .a5 YES
- .a6 YES
- .b3 YES
- .b4 YES
- .b9 YES
- .b10 YES
+ job10 job11 job12 job13 job14 job15
team86.a9 YES
team86.b6 YES
team91.a10 YES
team91.b1 YES
- .a4 YES
- .a7 YES
- .b2 YES
- .b5 YES
注意模型不是很小:
MODEL STATISTICS
BLOCKS OF EQUATIONS 3 SINGLE EQUATIONS 36
BLOCKS OF VARIABLES 2 SINGLE VARIABLES 1,801
NON ZERO ELEMENTS 6,901 DISCRETE VARIABLES 1,800
但是,MIP 模型很容易求解:不到一秒。
我没有在大型数据集上测试模型。这只是为了展示如何解决这样的问题。
【讨论】:
能否请您添加用于解决 MIP 模型问题的库? 我使用了 GAMS/Cplex。但这是一个通用的 MIP 模型,所以任何 MIP 求解器都可以(至少对于较小的实例)。大型实例可能需要更多思考。【参考方案3】:您可以尝试一个明显的启发式方法:
-
使用Hungarian Algorithm解决经典问题,
然后使用未分配的代理池,为每个代理找到改进最大的组合分配。
当然不是最优的,但比单独的匈牙利算法明显一阶改进。
【讨论】:
我们每个作业大约有 2 个代理,因此未分配的代理池可能很大,因此您建议的解决方案在计算上变得昂贵。谢谢,仍然会在模拟中尝试这种方法 @ArtyomAkselrod 计算量真的没有那么大,它只是一个额外的+O(n^2)
,而匈牙利算法本身已经是O(n^3)
或O(n^4)
,所以它不会改变大O。以上是关于每个工作有 2 个工人的分配问题的主要内容,如果未能解决你的问题,请参考以下文章