이 섹션에서는 MIP 또는 CP-SAT 솔버보다 빠를 수 있는 단순한 할당 문제의 전문 솔버인 선형 합계 할당 솔버에 관해 설명합니다. 하지만 MIP 및 CP-SAT 솔버는 훨씬 다양한 문제를 처리할 수 있으므로 대부분의 경우 최상의 옵션입니다.
비용 매트릭스
작업자 및 태스크에 대한 비용은 아래 표에 나와 있습니다.
작업자 | 작업 0 | 작업 1 | 작업 2 | 작업 3 |
---|---|---|---|---|
0 | 90 | 76 | 75 | 70 |
1 | 35 | 85 | 55 | 65 |
2 | 125 | 95 | 90 | 105 |
3 | 45 | 110 | 95 | 115 |
다음 섹션에서는 선형 합계 대입 솔버를 사용하여 대입 문제를 해결하는 Python 프로그램을 보여줍니다.
라이브러리 가져오기
다음은 필요한 라이브러리를 가져오는 코드입니다.
Python
import numpy as np from ortools.graph.python import linear_sum_assignment
C++
#include "ortools/graph/assignment.h" #include <cstdint> #include <numeric> #include <string> #include <vector>
Java
import com.google.ortools.Loader; import com.google.ortools.graph.LinearSumAssignment; import java.util.stream.IntStream;
C#
using System; using System.Collections.Generic; using System.Linq; using Google.OrTools.Graph;
데이터 정의
다음 코드는 프로그램의 데이터를 만듭니다.
Python
costs = np.array( [ [90, 76, 75, 70], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115], ] ) # Let's transform this into 3 parallel vectors (start_nodes, end_nodes, # arc_costs) end_nodes_unraveled, start_nodes_unraveled = np.meshgrid( np.arange(costs.shape[1]), np.arange(costs.shape[0]) ) start_nodes = start_nodes_unraveled.ravel() end_nodes = end_nodes_unraveled.ravel() arc_costs = costs.ravel()
C++
const int num_workers = 4; std::vector<int> all_workers(num_workers); std::iota(all_workers.begin(), all_workers.end(), 0); const int num_tasks = 4; std::vector<int> all_tasks(num_tasks); std::iota(all_tasks.begin(), all_tasks.end(), 0); const std::vector<std::vector<int>> costs = {{ {{90, 76, 75, 70}}, // Worker 0 {{35, 85, 55, 65}}, // Worker 1 {{125, 95, 90, 105}}, // Worker 2 {{45, 110, 95, 115}}, // Worker 3 }};
Java
final int[][] costs = { {90, 76, 75, 70}, {35, 85, 55, 65}, {125, 95, 90, 105}, {45, 110, 95, 115}, }; final int numWorkers = 4; final int numTasks = 4; final int[] allWorkers = IntStream.range(0, numWorkers).toArray(); final int[] allTasks = IntStream.range(0, numTasks).toArray();
C#
int[,] costs = { { 90, 76, 75, 70 }, { 35, 85, 55, 65 }, { 125, 95, 90, 105 }, { 45, 110, 95, 115 }, }; int numWorkers = 4; int[] allWorkers = Enumerable.Range(0, numWorkers).ToArray(); int numTasks = 4; int[] allTasks = Enumerable.Range(0, numTasks).ToArray();
배열은 비용 행렬이고, i, j 항목은 작업자 i가 작업 j를 수행하는 비용입니다. 행렬의 행에 해당하는 4개의 작업자와 열에 해당하는 4개의 태스크가 있습니다.
문제 해결사 만들기
이 프로그램은 할당 문제의 특수 솔버인 선형 할당 솔버를 사용합니다.
다음 코드는 솔버를 만듭니다.
Python
assignment = linear_sum_assignment.SimpleLinearSumAssignment()
C++
SimpleLinearSumAssignment assignment;
Java
LinearSumAssignment assignment = new LinearSumAssignment();
C#
LinearSumAssignment assignment = new LinearSumAssignment();
제약조건 추가
다음 코드는 worker와 태스크를 루프 처리하여 솔버에 비용을 추가합니다.
Python
assignment.add_arcs_with_cost(start_nodes, end_nodes, arc_costs)
C++
for (int w : all_workers) { for (int t : all_tasks) { if (costs[w][t]) { assignment.AddArcWithCost(w, t, costs[w][t]); } } }
Java
// Add each arc. for (int w : allWorkers) { for (int t : allTasks) { if (costs[w][t] != 0) { assignment.addArcWithCost(w, t, costs[w][t]); } } }
C#
// Add each arc. foreach (int w in allWorkers) { foreach (int t in allTasks) { if (costs[w, t] != 0) { assignment.AddArcWithCost(w, t, costs[w, t]); } } }
솔버 호출
다음 코드는 솔버를 호출합니다.
Python
status = assignment.solve()
C++
SimpleLinearSumAssignment::Status status = assignment.Solve();
Java
LinearSumAssignment.Status status = assignment.solve();
C#
LinearSumAssignment.Status status = assignment.Solve();
결과 표시
다음 코드는 솔루션을 보여줍니다.
Python
if status == assignment.OPTIMAL: print(f"Total cost = {assignment.optimal_cost()}\n") for i in range(0, assignment.num_nodes()): print( f"Worker {i} assigned to task {assignment.right_mate(i)}." + f" Cost = {assignment.assignment_cost(i)}" ) elif status == assignment.INFEASIBLE: print("No assignment is possible.") elif status == assignment.POSSIBLE_OVERFLOW: print("Some input costs are too large and may cause an integer overflow.")
C++
if (status == SimpleLinearSumAssignment::OPTIMAL) { LOG(INFO) << "Total cost: " << assignment.OptimalCost(); for (int worker : all_workers) { LOG(INFO) << "Worker " << std::to_string(worker) << " assigned to task " << std::to_string(assignment.RightMate(worker)) << ". Cost: " << std::to_string(assignment.AssignmentCost(worker)) << "."; } } else { LOG(INFO) << "Solving the linear assignment problem failed."; }
Java
if (status == LinearSumAssignment.Status.OPTIMAL) { System.out.println("Total cost: " + assignment.getOptimalCost()); for (int worker : allWorkers) { System.out.println("Worker " + worker + " assigned to task " + assignment.getRightMate(worker) + ". Cost: " + assignment.getAssignmentCost(worker)); } } else { System.out.println("Solving the min cost flow problem failed."); System.out.println("Solver status: " + status); }
C#
if (status == LinearSumAssignment.Status.OPTIMAL) { Console.WriteLine($"Total cost: {assignment.OptimalCost()}."); foreach (int worker in allWorkers) { Console.WriteLine($"Worker {worker} assigned to task {assignment.RightMate(worker)}. " + $"Cost: {assignment.AssignmentCost(worker)}."); } } else { Console.WriteLine("Solving the linear assignment problem failed."); Console.WriteLine($"Solver status: {status}."); }
아래 출력은 작업에 대한 최적의 worker 할당을 보여줍니다.
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 Time = 0.000147 seconds
다음 그래프는 해답을 그래프의 파선 가장자리로 보여줍니다. 점선으로 된 가장자리 옆의 숫자가 비용입니다. 이 할당의 총 대기 시간은 파선 비용의 합계인 265입니다.
그래프 이론에서는 왼쪽에 있는 모든 노드와 오른쪽에 있는 노드가 정확히 한 개 있는 이분판 그래프에서 에지의 집합을 완전 일치라고 합니다.
전체 프로그램
전체 프로그램은 다음과 같습니다.
Python
"""Solve assignment problem using linear assignment solver.""" import numpy as np from ortools.graph.python import linear_sum_assignment def main(): """Linear Sum Assignment example.""" assignment = linear_sum_assignment.SimpleLinearSumAssignment() costs = np.array( [ [90, 76, 75, 70], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115], ] ) # Let's transform this into 3 parallel vectors (start_nodes, end_nodes, # arc_costs) end_nodes_unraveled, start_nodes_unraveled = np.meshgrid( np.arange(costs.shape[1]), np.arange(costs.shape[0]) ) start_nodes = start_nodes_unraveled.ravel() end_nodes = end_nodes_unraveled.ravel() arc_costs = costs.ravel() assignment.add_arcs_with_cost(start_nodes, end_nodes, arc_costs) status = assignment.solve() if status == assignment.OPTIMAL: print(f"Total cost = {assignment.optimal_cost()}\n") for i in range(0, assignment.num_nodes()): print( f"Worker {i} assigned to task {assignment.right_mate(i)}." + f" Cost = {assignment.assignment_cost(i)}" ) elif status == assignment.INFEASIBLE: print("No assignment is possible.") elif status == assignment.POSSIBLE_OVERFLOW: print("Some input costs are too large and may cause an integer overflow.") if __name__ == "__main__": main()
C++
#include "ortools/graph/assignment.h" #include <cstdint> #include <numeric> #include <string> #include <vector> namespace operations_research { // Simple Linear Sum Assignment Problem (LSAP). void AssignmentLinearSumAssignment() { SimpleLinearSumAssignment assignment; const int num_workers = 4; std::vector<int> all_workers(num_workers); std::iota(all_workers.begin(), all_workers.end(), 0); const int num_tasks = 4; std::vector<int> all_tasks(num_tasks); std::iota(all_tasks.begin(), all_tasks.end(), 0); const std::vector<std::vector<int>> costs = {{ {{90, 76, 75, 70}}, // Worker 0 {{35, 85, 55, 65}}, // Worker 1 {{125, 95, 90, 105}}, // Worker 2 {{45, 110, 95, 115}}, // Worker 3 }}; for (int w : all_workers) { for (int t : all_tasks) { if (costs[w][t]) { assignment.AddArcWithCost(w, t, costs[w][t]); } } } SimpleLinearSumAssignment::Status status = assignment.Solve(); if (status == SimpleLinearSumAssignment::OPTIMAL) { LOG(INFO) << "Total cost: " << assignment.OptimalCost(); for (int worker : all_workers) { LOG(INFO) << "Worker " << std::to_string(worker) << " assigned to task " << std::to_string(assignment.RightMate(worker)) << ". Cost: " << std::to_string(assignment.AssignmentCost(worker)) << "."; } } else { LOG(INFO) << "Solving the linear assignment problem failed."; } } } // namespace operations_research int main() { operations_research::AssignmentLinearSumAssignment(); return EXIT_SUCCESS; }
Java
package com.google.ortools.graph.samples; import com.google.ortools.Loader; import com.google.ortools.graph.LinearSumAssignment; import java.util.stream.IntStream; /** Minimal Linear Sum Assignment problem. */ public class AssignmentLinearSumAssignment { public static void main(String[] args) { Loader.loadNativeLibraries(); LinearSumAssignment assignment = new LinearSumAssignment(); final int[][] costs = { {90, 76, 75, 70}, {35, 85, 55, 65}, {125, 95, 90, 105}, {45, 110, 95, 115}, }; final int numWorkers = 4; final int numTasks = 4; final int[] allWorkers = IntStream.range(0, numWorkers).toArray(); final int[] allTasks = IntStream.range(0, numTasks).toArray(); // Add each arc. for (int w : allWorkers) { for (int t : allTasks) { if (costs[w][t] != 0) { assignment.addArcWithCost(w, t, costs[w][t]); } } } LinearSumAssignment.Status status = assignment.solve(); if (status == LinearSumAssignment.Status.OPTIMAL) { System.out.println("Total cost: " + assignment.getOptimalCost()); for (int worker : allWorkers) { System.out.println("Worker " + worker + " assigned to task " + assignment.getRightMate(worker) + ". Cost: " + assignment.getAssignmentCost(worker)); } } else { System.out.println("Solving the min cost flow problem failed."); System.out.println("Solver status: " + status); } } private AssignmentLinearSumAssignment() {} }
C#
using System; using System.Collections.Generic; using System.Linq; using Google.OrTools.Graph; public class AssignmentLinearSumAssignment { static void Main() { LinearSumAssignment assignment = new LinearSumAssignment(); int[,] costs = { { 90, 76, 75, 70 }, { 35, 85, 55, 65 }, { 125, 95, 90, 105 }, { 45, 110, 95, 115 }, }; int numWorkers = 4; int[] allWorkers = Enumerable.Range(0, numWorkers).ToArray(); int numTasks = 4; int[] allTasks = Enumerable.Range(0, numTasks).ToArray(); // Add each arc. foreach (int w in allWorkers) { foreach (int t in allTasks) { if (costs[w, t] != 0) { assignment.AddArcWithCost(w, t, costs[w, t]); } } } LinearSumAssignment.Status status = assignment.Solve(); if (status == LinearSumAssignment.Status.OPTIMAL) { Console.WriteLine($"Total cost: {assignment.OptimalCost()}."); foreach (int worker in allWorkers) { Console.WriteLine($"Worker {worker} assigned to task {assignment.RightMate(worker)}. " + $"Cost: {assignment.AssignmentCost(worker)}."); } } else { Console.WriteLine("Solving the linear assignment problem failed."); Console.WriteLine($"Solver status: {status}."); } } }
작업자가 일부 태스크를 수행할 수 없는 경우 솔루션
이전 예에서는 모든 작업자가 모든 태스크를 수행할 수 있다고 가정했습니다. 하지만 항상 그런 것은 아닙니다. 작업자는 다양한 이유로 하나 이상의 작업을 수행하지 못할 수 있습니다. 그러나 위 프로그램을 쉽게 수정하여 이를 처리할 수 있습니다.
예를 들어 작업자 0이 태스크 3을 수행할 수 없다고 가정해 보겠습니다. 이를 고려하여 프로그램을 수정하려면 다음과 같이 변경합니다.
- 비용 행렬의 0, 3 항목을
'NA'
문자열로 변경합니다. (모든 문자열을 사용할 수 있습니다.)cost = [[90, 76, 75, 'NA'], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115]]
- 솔버에 비용을 할당하는 코드 섹션에서 아래와 같이
if cost[worker][task] != 'NA':
줄을 추가합니다.for worker in range(0, rows): for task in range(0, cols): if cost[worker][task] != 'NA': assignment.AddArcWithCost(worker, task, cost[worker][task])
추가된 선은 비용 행렬의 항목이'NA'
인 가장자리가 솔버에 추가되지 않도록 합니다.
이렇게 변경하고 수정된 코드를 실행하면 다음과 같은 출력이 표시됩니다.
Total cost = 276 Worker 0 assigned to task 1. Cost = 76 Worker 1 assigned to task 3. Cost = 65 Worker 2 assigned to task 2. Cost = 90 Worker 3 assigned to task 0. Cost = 45
총비용이 원래 문제보다 높다는 것을 알 수 있습니다. 이는 원래 문제에서 최적의 솔루션이 작업자 0을 태스크 3에 할당했지만 수정된 문제에서는 할당이 허용되지 않기 때문입니다.
더 많은 작업자가 작업을 수행할 수 없으면 어떻게 되는지 알아보려면 비용 행렬의 더 많은 항목을 'NA'
로 바꿔 특정 작업을 수행할 수 없는 추가 작업자를 표시하면 됩니다.
cost = [[90, 76, 'NA', 'NA'], [35, 85, 'NA', 'NA'], [125, 95, 'NA','NA'], [45, 110, 95, 115]]
이번에 프로그램을 실행하면 다음과 같은 음수 결과가 나옵니다.
No assignment is possible.
즉, 각 작업자가 다른 태스크를 수행하기 위해 작업자를 태스크에 할당할 수 있는 방법이 없습니다. 문제의 그래프를 보면 그 이유를 알 수 있습니다 (비용 행렬에서 'NA'
값에 해당하는 에지가 없음).
세 작업자 0, 1, 2의 노드는 작업 0과 1의 두 노드에만 연결되므로 이러한 작업자에 고유한 작업을 할당할 수 없습니다.
결혼의 정리
그래프 이론에는 혼합 정리라는 잘 알려진 결과가 있는데, 이 결과는 위와 같이 이분방 그래프에서 왼쪽의 각 노드를 오른쪽의 고유한 노드에 할당할 수 있는 시점을 정확히 알려줍니다. 이러한 할당을 완벽한 일치라고 합니다. 간단히 말해, 이 정리는 이전 예의 노드와 같이 왼쪽에 그 에지가 오른쪽에 있는 더 작은 노드 집합으로 이어지는 노드의 하위 집합이 없는 경우 이 연산이 가능하다는 것입니다.
더 정확히 말하면 이 정리는 그래프 왼쪽에 있는 노드의 하위 집합 S에 대해 S의 노드에 에지로 연결된 그래프의 오른쪽에 있는 노드 집합이 최소한 S보다 크다면 이분 그래프가 완전 일치한다고 말합니다.