Перейти до основного вмісту

Розв'язання задачі про призначення за допомогою потоку мінімальної вартості

Задача про призначення має два рівносильні формулювання:

  • Дано квадратну матрицю A[1..N,1..N]A[1..N, 1..N], потрібно вибрати в ній NN елементів так, щоб у кожному рядку та кожному стовпці було вибрано рівно один елемент, а сума значень цих елементів була найменшою.
  • Є NN замовлень і NN верстатів. Для кожного замовлення відома вартість виготовлення на кожному з верстатів. На кожному верстаті можна виконати лише одне замовлення. Потрібно призначити всі замовлення на верстати так, щоб сумарна вартість була мінімальною.

Тут ми розглянемо розв'язання цієї задачі на основі алгоритму пошуку потоку мінімальної вартості (min-cost-flow), який розв'язує задачу про призначення за O(N3)\mathcal{O}(N^3).

Коли підходить цей підхід?
  • Це задача про призначення (досконале парування мінімальної вартості у дводольному графі), і ви вже впевнено володієте технікою потоку мінімальної вартості? (якщо потрібен компактніший спеціалізований розв'язок → Угорський алгоритм)
  • Окрім самого призначення, корисно мати під рукою загальний каркас min-cost-flow для споріднених варіацій задачі?
  • Ребра мають вартості (а не лише наявність)? (якщо ваг немає і потрібне максимальне парування → алгоритм Куна)

Опис

Побудуємо двочасткову мережу: є джерело SS, стік TT, у першій частці є NN вершин (що відповідають рядкам матриці, або замовленням), у другій також є NN вершин (що відповідають стовпцям матриці, або верстатам). Між кожною вершиною ii першої множини та кожною вершиною jj другої множини проведемо ребро з пропускною здатністю 1 і вартістю AijA_{ij}. Із джерела SS проведемо ребра до всіх вершин ii першої множини з пропускною здатністю 1 і вартістю 0. Із кожної вершини другої множини jj проведемо ребро з пропускною здатністю 1 і вартістю 0 до стоку TT.

У побудованій мережі знаходимо максимальний потік мінімальної вартості. Очевидно, що величина потоку дорівнюватиме NN. Далі, для кожної вершини ii першої частки існує рівно одна вершина jj другої частки, така, що потік FijF_{ij} = 1. Зрештою, це взаємно однозначна відповідність між вершинами першої частки та вершинами другої частки, яка і є розв'язком задачі (оскільки знайдений потік має мінімальну вартість, то сума вартостей вибраних ребер буде найменшою з можливих, що і є критерієм оптимальності).

Складність такого розв'язання задачі про призначення залежить від алгоритму, яким виконується пошук максимального потоку мінімальної вартості. Складність буде O(N3)\mathcal{O}(N^3) при використанні Дейкстри або O(N4)\mathcal{O}(N^4) при використанні Беллмана-Форда. Це пов'язано з тим, що потік має розмір O(N)O(N), і кожна ітерація алгоритму Дейкстри може виконуватися за O(N2)O(N^2), тоді як для Беллмана-Форда це O(N3)O(N^3).

Реалізація

Наведена тут реалізація доволі громіздка, її напевно можна суттєво скоротити. Вона використовує алгоритм SPFA для пошуку найкоротших шляхів.

const int INF = 1000 * 1000 * 1000;

vector<int> assignment(vector<vector<int>> a) {
int n = a.size();
int m = n * 2 + 2;
vector<vector<int>> f(m, vector<int>(m));
int s = m - 2, t = m - 1;
int cost = 0;
while (true) {
vector<int> dist(m, INF);
vector<int> p(m);
vector<bool> inq(m, false);
queue<int> q;
dist[s] = 0;
p[s] = -1;
q.push(s);
while (!q.empty()) {
int v = q.front();
q.pop();
inq[v] = false;
if (v == s) {
for (int i = 0; i < n; ++i) {
if (f[s][i] == 0) {
dist[i] = 0;
p[i] = s;
inq[i] = true;
q.push(i);
}
}
} else {
if (v < n) {
for (int j = n; j < n + n; ++j) {
if (f[v][j] < 1 && dist[j] > dist[v] + a[v][j - n]) {
dist[j] = dist[v] + a[v][j - n];
p[j] = v;
if (!inq[j]) {
q.push(j);
inq[j] = true;
}
}
}
} else {
for (int j = 0; j < n; ++j) {
if (f[v][j] < 0 && dist[j] > dist[v] - a[j][v - n]) {
dist[j] = dist[v] - a[j][v - n];
p[j] = v;
if (!inq[j]) {
q.push(j);
inq[j] = true;
}
}
}
}
}
}

int curcost = INF;
for (int i = n; i < n + n; ++i) {
if (f[i][t] == 0 && dist[i] < curcost) {
curcost = dist[i];
p[t] = i;
}
}
if (curcost == INF)
break;
cost += curcost;
for (int cur = t; cur != -1; cur = p[cur]) {
int prev = p[cur];
if (prev != -1)
f[cur][prev] = -(f[prev][cur] = 1);
}
}

vector<int> answer(n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (f[i][j + n] == 1)
answer[i] = j;
}
}
return answer;
}

Відеоматеріали