每个工作 2 个工人的分配问题

问题描述

问题设置

目前,我们正在为一家食品技术初创公司(电子杂货店)解决调度问题。我们有工作(要交付的订单)和工人(快递员/包装员/通用) 问题是有效地将订单分配给工人。第一步,我们决定优化 CTE(即点即食 - 下订单和订单交付之间的时间)

问题本身

问题来自这样一个事实,有时每个工作有 2 个工人而不是一个执行者是有效的,因为打包员可能知道商店“地图”,而快递员可能有自行车,与每个工作相比,它可以更快地执行工作他们单独考虑订单转移成本。

我们研究了算法,发现我们的问题看起来像assignment problem,并且有一个算法解决方案(Hungarian algorithm),但问题是经典问题需要“每个工作分配给一个工人,并且每个工人被分配一份工作”,而在我们的例子中,有时每个工作有 2 个工人是有效率的。

到目前为止我们尝试过的

  1. 插入(packer A + universal B)组合到成本矩阵中,但在这种情况下我们不能添加universal B 进入矩阵,因为结果我们可以得到 universal B 将被分配给 2 个工作(作为一个单独的单元,作为与 packer A 组合的一部分)

  2. 因此实施 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()

解决方法

我用一个可以处理团队的模型进行了快速而肮脏的测试。仅用于说明目的。

我创建了两种类型的工人: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,a10,b1,b5 
b6,b10


----     13 SET a  a workers

a1,a10


----     13 SET b  b workers

b1,b10


----     13 SET t  teams

team1,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)

我尝试将其建模为混合整数编程模型如下:

enter image description here

显然,这不再是一个纯粹的赋值问题。第一个约束比我们习惯的要复杂。它说对于每个工人 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 模型很容易求解:不到一秒。

我没有在大型数据集上测试模型。只是为了展示如何解决这样的问题。

,

您可以尝试一个明显的启发式方法:

  1. 使用 Hungarian Algorithm 解决经典问题,
  2. 然后使用未分配的代理池,为每个代理找到能带来最大改进的组合分配。

当然不是最优的,而是比单独的匈牙利算法有明显的一阶改进。

,

匈牙利算法是一种过时的解决方案,由于一些我不明白的莫名其妙的原因(可能是因为它在概念上很简单),它仍然很受欢迎。

使用最小成本流为您的问题建模。它更加灵活,并且具有许多有效的算法。也可以证明它可以解决匈牙利算法可以解决的任何问题(证明很简单。)

鉴于对您的问题的描述非常模糊,您可能希望使用两层节点 V = (O,W) 对底层图 G=(V,E) 进行建模,其中 O 是订单,W 是工人.

边可以被定向,每个 Worker 都有一个边,容量为 1,可以满足每个可能的顺序。将源节点连接到边缘容量为 1 的工作节点,并将每个订单节点连接到容量为 2(或更高,允许每个订单有更多工作节点)的汇节点。

我上面描述的实际上是一个 maxflow 实例而不是 MCF,因为它没有分配权重。但是,您可以为任何边分配权重。

鉴于您的问题表述,我不明白这甚至是一个分配问题,您能否不使用简单的先来先分配(队列)类型策略,因为您似乎没有任何标准来拥有工人更喜欢按某种顺序工作。