1. 程式人生 > >運籌系列14:Assignment問題模型與python程式碼求解

運籌系列14:Assignment問題模型與python程式碼求解

1. 問題描述

分配問題可以簡單描述為:有數個人和數個任務,人做任務有不同的費用。每個人最多隻能做一項任務,每個任務只能由一個人做。如何將任務分配給人可以使總費用最小?
用數學語言表示為:
m i n min Σ

i I , j J c
i j
x i j \Sigma_{i\in I,j\in J}c_{ij}x_{ij}

s.t.
Σ i I x i j = 1 , j J \Sigma_{i\in I} x_{ij} =1, \forall j\in J
Σ j J x i j 1 , i I \Sigma_{j\in J} x_{ij} \leq 1, \forall i\in I
來看一個例子,有4個人和4個任務,費用矩陣如下表:

人\任務 0 1 2 3
0 90 76 75 70
1 35 85 55 65
2 125 95 90 105
3 45 110 95 115

ortools有現成的模型LinearSumAssignment可以使用,下面是python程式碼:

from ortools.graph import pywrapgraph
cost = [[90, 76, 75, 70],[35, 85, 55, 65],[125, 95, 90, 105],[45, 110, 95, 115]]
rows = len(cost)
cols = len(cost[0])

assignment = pywrapgraph.LinearSumAssignment()
for worker in range(rows):
	for task in range(cols):
		if cost[worker][task]:
			assignment.AddArcWithCost(worker, task, cost[worker][task])
solve_status = assignment.Solve()
if solve_status == assignment.OPTIMAL:
	print('Total cost = ', assignment.OptimalCost())
	print()
	for i in range(0, assignment.NumNodes()):
		print('Worker %d assigned to task %d.  Cost = %d' % (i,assignment.RightMate(i),assignment.AssignmentCost(i)))
elif solve_status == assignment.INFEASIBLE:
	print('No assignment is possible.')
elif solve_status == assignment.POSSIBLE_OVERFLOW:
	print('Some input costs are too large and may cause an integer overflow.')

輸出結果為:

Total cost =  265

Worker 0 assigned to task 3.  Cost = 70
Worker 1 assigned to task 2.  Cost = 55
Worker 2 assigned to task 1.  Cost = 95
Worker 3 assigned to task 0.  Cost = 45

2. 問題變形1

假設我們有2個團隊(每個團隊中3個人)和4個任務,要求每個團隊分配2個任務使得總費用最小該怎麼建模?
這種情況下,我們可以將問題建模為最小費用流問題,如下圖:
在這裡插入圖片描述
最小費用流的求解方法見上一篇文章。

3. 問題變形2

有時候任務本身有一個size的屬性,比如每項工作需要一定的時間或費用,任務分配的時候不是限制每個人一個任務,而是要求task_size的和不能超過人能處理的size_max的限制。這裡可以使用前面文章提到的CP進行建模求解:

from __future__ import print_function
from ortools.sat.python import cp_model
import numpy as np

model = cp_model.CpModel()
start = time.time()
cost = [[90, 76, 75, 70, 50, 74, 12, 68],
        [35, 85, 55, 65, 48, 101, 70, 83],
        [125, 95, 90, 105, 59, 120, 36, 73],
        [45, 110, 95, 115, 104, 83, 37, 71],
        [60, 105, 80, 75, 59, 62, 93, 88],
        [45, 65, 110, 95, 47, 31, 81, 34],
        [38, 51, 107, 41, 69, 99, 115, 48],
        [47, 85, 57, 71, 92, 77, 109, 36],
        [39, 63, 97, 49, 118, 56, 92, 61],
        [47, 101, 71, 60, 88, 109, 52, 90]]
sizes = [10, 7, 3, 12, 15, 4, 11, 5]
total_size_max = 15
num_workers = len(cost)
num_tasks = len(cost[1])
# Variables
x = []
for i in range(num_workers):
  t = []
  for j in range(num_tasks):
    t.append(model.NewIntVar(0, 1, "x[%i,%i]" % (i, j)))
  x.append(t)
x_array = [x[i][j] for i in range(num_workers) for j in range(num_tasks)]

# Constraints

# Each task is assigned to at least one worker.
[model.Add(sum(x[i][j] for i in range(num_workers)) >= 1)
for j in range(num_tasks)]

# Total size of tasks for each worker is at most total_size_max.

[model.Add(sum(sizes[j] * x[i][j] for j in range(num_tasks)) <= total_size_max)
for i in range(num_workers)]
model.Minimize(sum([np.dot(x_row, cost_row) for (x_row, cost_row) in zip(x, cost)]))
solver = cp_model.CpSolver()
status = solver.Solve(model)

if status == cp_model.OPTIMAL:
  print('Minimum cost = %i' % solver.ObjectiveValue())
  print()

  for i in range(num_workers):
    for j in range(num_tasks):
      if solver.Value(x[i][j]) == 1:
        print('Worker ', i, ' assigned to task ', j, '  Cost = ', cost[i][j])
  print()
  end = time.time()
  print("Time = ", round(end - start, 4), "seconds")

輸出為:

Minimum cost: 326

Worker  0  assigned to task  6   Cost =  12
Worker  1  assigned to task  0   Cost =  35
Worker  1  assigned to task  2   Cost =  55
Worker  2  assigned to task  4   Cost =  59
Worker  5  assigned to task  5   Cost =  31
Worker  5  assigned to task  7   Cost =  34
Worker  6  assigned to task  1   Cost =  51
Worker  8  assigned to task  3   Cost =  49

4. 使用MIP方法進行求解

一般來說,就求解速度來說LinearSumAssignment<minCostFlow<CP<MIP,而求解問題的範圍則是反過來的。這裡對上一節的問題使用傳統的MIP進行建模求解:

cost = [[90, 76, 75, 70, 50, 74, 12, 68],
          [35, 85, 55, 65, 48, 101, 70, 83],
          [125, 95, 90, 105, 59, 120, 36, 73],
          [45, 110, 95, 115, 104, 83, 37, 71],
          [60, 105, 80, 75, 59, 62, 93, 88],
          [45, 65, 110, 95, 47, 31, 81, 34],
          [38, 51, 107, 41, 69, 99, 115, 48],
          [47, 85, 57, 71, 92, 77, 109, 36],
          [39, 63, 97, 49, 118, 56, 92, 61],
          [47, 101, 71, 60, 88, 109, 52, 90]]
task_sizes = [10, 7, 3, 12, 15, 4, 11, 5]
total_size_max = 15
from ortools.linear_solver import pywraplp
solver = pywraplp.Solver('SolveAssignmentProblem',pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)

# Maximum total of task sizes for any worker
num_workers = len(cost)
num_tasks = len(cost[1])
# Variables
x = {}
for i in range(num_workers):
    for j in range(num_tasks):
        x[i, j] = solver.IntVar(0, 1, 'x[%i,%i]' % (i, j))

# Constraints
# The total size of the tasks each worker takes on is at most total_size_max.
for i in range(num_workers):
    solver.Add(solver.Sum([task_sizes[j] * x[i, j] for j in range(num_tasks)]) <= total_size_max)

# Each task is assigned to at least one worker.
for j in range(num_tasks):
    solver.Add(solver.Sum([x[i, j] for i in range(num_workers)]) >= 1)

solver.Minimize(solver.Sum([cost[i][j] * x[i,j] for i in range(num_workers) for j in range(num_tasks)]))
sol = solver.Solve()

print('Minimum cost = ', solver.Objective().Value())
print()
for i in range(num_workers):
    for j in range(num_tasks):
        if x[i, j].solution_value() > 0:
            print('Worker', i,' assigned to task', j, '  Cost = ', cost[i][j])

結果為:

Minimum cost =  326.0

Worker 0  assigned to task 6   Cost =  12
Worker 1  assigned to task 0   Cost =  35
Worker 1  assigned to task 2   Cost =  55
Worker 4  assigned to task 4   Cost =  59
Worker 5  assigned to task 5   Cost =  31
Worker 5  assigned to task 7   Cost =  34
Worker 6  assigned to task 1   Cost =  51
Worker 8  assigned to task 3   Cost =  49