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

Планування задач на двох машинах

Ця задача полягає в пошуку оптимального розкладу для nn задач на двох машинах. Кожен елемент спершу має бути оброблений на першій машині, а вже потім на другій. ii-та задача займає aia_i часу на першій машині та bib_i часу на другій машині. Кожна машина може обробляти лише одну задачу за раз.

Ми хочемо знайти оптимальний порядок задач так, щоб підсумковий час обробки був якомога меншим.

Розв'язок, який ми тут обговорюємо, називається правилом Джонсона (на честь S. M. Johnson).

Варто зауважити, що задача стає NP-повною, якщо ми маємо більш ніж дві машини.

Коли підходить цей алгоритм?
  • Чи машин рівно дві, і кожна задача проходить їх у фіксованому порядку (спершу перша, потім друга)? (якщо машина одна → Планування робіт на одній машині)
  • Чи мета — мінімізувати загальний час завершення (makespan), а не штраф за очікування?
  • Чи відомі для кожної задачі обидва часи aia_i та bib_i, щоб застосувати компаратор min(ai,bi)\min(a_i, b_i) правила Джонсона?

Побудова алгоритму

Зауважимо спершу, що ми можемо вважати, що порядок задач для першої та другої машини має збігатися. Справді, оскільки задачі для другої машини стають доступними після їх обробки на першій, і якщо є кілька задач, доступних для другої машини, то час обробки дорівнюватиме сумі їхніх bib_i незалежно від їхнього порядку. Тому вигідно відправляти задачі на другу машину в тому самому порядку, в якому ми відправляли їх на першу машину.

Розглянемо порядок задач, що збігається з порядком їх подачі на вхід 1,2,,n1, 2, \dots, n.

Через xix_i позначимо час простою другої машини безпосередньо перед обробкою ii. Наша мета — мінімізувати загальний час простою:

F(x)=xi minF(x) = \sum x_i ~ \rightarrow \min

Для першої задачі маємо x1=a1x_1 = a_1. Для другої задачі, оскільки вона відправляється на машину в момент a1+a2a_1 + a_2, а друга машина звільняється в момент x1+b1x_1 + b_1, маємо x2=max((a1+a2)(x1+b1),0)x_2 = \max\left((a_1 + a_2) - (x_1 + b_1), 0\right). У загальному випадку отримуємо рівняння:

xk=max(i=1kaii=1k1bii=1k1xi,0)x_k = \max\left(\sum_{i=1}^k a_i - \sum_{i=1}^{k-1} b_i - \sum_{i=1}^{k-1} x_i, 0 \right)

Тепер ми можемо обчислити загальний час простою F(x)F(x). Стверджується, що він має вигляд

F(x)=maxk=1nKi,F(x) = \max_{k=1 \dots n} K_i,

де

Ki=i=1kaii=1k1bi.K_i = \sum_{i=1}^k a_i - \sum_{i=1}^{k-1} b_i.

Це легко перевірити за допомогою індукції.

Тепер ми скористаємося методом перестановок: ми поміняємо місцями дві сусідні задачі jj та j+1j+1 і подивимося, як це змінить загальний час простою.

З вигляду виразу для KiK_i зрозуміло, що змінюються лише KjK_j та Kj+1K_{j+1}; позначимо їхні нові значення через KjK_j' та Kj+1K_{j+1}'.

Якщо ця зміна порядку задач jj та j+1j+1 збільшила загальний час простою, то має виконуватися:

max(Kj,Kj+1)max(Kj,Kj+1)\max(K_j, K_{j+1}) \le \max(K_j', K_{j+1}')

(Перестановка двох задач також може взагалі не вплинути на результат. Наведена умова є лише достатньою, але не необхідною.)

Після віднімання i=1j+1aii=1j1bi\sum_{i=1}^{j+1} a_i - \sum_{i=1}^{j-1} b_i від обох частин нерівності отримуємо:

max(aj+1,bj)max(bj+1,aj)\max(-a_{j+1}, -b_j) \le \max(-b_{j+1}, -a_j)

А після позбавлення від знаків мінус:

min(aj,bj+1)min(bj,aj+1)\min(a_j, b_{j+1}) \le \min(b_j, a_{j+1})

Таким чином ми отримали компаратор: відсортувавши за ним задачі, ми отримаємо оптимальний порядок задач, у якому жодні дві задачі не можна поміняти місцями з покращенням підсумкового часу.

Однак сортування можна ще спростити, якщо поглянути на компаратор під іншим кутом. Компаратор можна інтерпретувати так: якщо ми маємо чотири значення часу (aj,aj+1,bj,bj+1)(a_j, a_{j+1}, b_j, b_{j+1}), і мінімум з них — це час, що відповідає першій машині, то відповідну задачу слід виконувати першою. Якщо ж мінімальний час — це час з другої машини, то її слід виконувати пізніше. Отже, ми можемо сортувати задачі за min(ai,bi)\min(a_i, b_i), і якщо час обробки поточної задачі на першій машині менший за час обробки на другій машині, то цю задачу треба виконати перед усіма рештою задач, інакше — після всіх решти задач.

Так чи інакше, виявляється, що за правилом Джонсона ми можемо розв'язати задачу сортуванням задач і таким чином отримати часову складність O(nlogn)O(n \log n).

Реалізація

Тут ми реалізуємо другий варіант описаного алгоритму.

struct Job {
int a, b, idx;

bool operator<(Job o) const {
return min(a, b) < min(o.a, o.b);
}
};

vector<Job> johnsons_rule(vector<Job> jobs) {
sort(jobs.begin(), jobs.end());
vector<Job> a, b;
for (Job j : jobs) {
if (j.a < j.b)
a.push_back(j);
else
b.push_back(j);
}
a.insert(a.end(), b.rbegin(), b.rend());
return a;
}

pair<int, int> finish_times(vector<Job> const& jobs) {
int t1 = 0, t2 = 0;
for (Job j : jobs) {
t1 += j.a;
t2 = max(t2, t1) + j.b;
}
return make_pair(t1, t2);
}

Уся інформація про кожну задачу зберігається в структурі. Перша функція сортує всі задачі та обчислює оптимальний розклад. Друга функція обчислює моменти завершення для обох машин за заданим розкладом.

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