Угорський алгоритм для розв'язання задачі про призначення
Постановка задачі про призначення
Існує кілька стандартних формулювань задачі про призначення (усі вони по суті еквівалентні). Ось деякі з них:
-
Є робіт і працівників. Кожен працівник вказує суму грошей, яку очікує за конкретну роботу. Кожного працівника можна призначити лише на одну роботу. Мета — розподілити роботи між працівниками так, щоб мінімізувати сумарну вартість.
-
Дано матрицю розміру . Потрібно вибрати по одному числу з кожного рядка так, щоб з кожного стовпця було вибрано рівно одне число, а сума вибраних чисел була мінімальною.
-
Дано матрицю розміру . Потрібно знайти перестановку довжини таку, що величина мінімальна.
-
Розглянемо повний двочастковий граф з вершинами в кожній частині, де кожному ребру призначено вагу. Мета — знайти досконале парування з мінімальною сумарною вагою.
Важливо зауважити, що всі наведені вище сценарії є "квадратними" задачами, тобто обидві розмірності завжди дорівнюють . На практиці часто трапляються подібні "прямокутні" формулювання, де не дорівнює , і потрібно вибрати елементів. Однак можна помітити, що "прямокутну" задачу завжди можна звести до "квадратної", додавши рядки або стовпці зі значеннями нуль чи нескінченність відповідно.
Також зауважимо, що за аналогією з пошуком мінімального розв'язку можна поставити й задачу пошуку максимального розв'язку. Однак ці дві задачі еквівалентні одна одній: достатньо помножити всі ваги на .
- Це задача про призначення: дано квадратну матрицю вартостей, і потрібно вибрати по одному елементу з кожного рядка та стовпця з мінімальною сумою (досконале парування мінімальної ваги у дводольному графі)?
- Ребра мають ваги (вартості)? (якщо ваг немає і потрібне лише максимальне за потужністю парування → алгоритм Куна)
- Потрібен компактний прямий алгоритм за ? (ту саму задачу можна звести до потоку мінімальної вартості, але це зазвичай повільніше й громіздкіше)
Угорський алгоритм
Історична довідка
Алгоритм був розроблений і опублікований Гарольдом Куном (Harold Kuhn) у 1955 році. Сам Кун дав йому назву "угорський", оскільки той ґрунтувався на попередніх роботах угорських математиків Денеша Кеніга (Dénes Kőnig) та Єне Егерварі (Jenő Egerváry).
У 1957 році Джеймс Манкрес (James Munkres) показав, що цей алгоритм працює за (строго) поліноміальний час, незалежно від вартостей.
Тому в літературі цей алгоритм відомий не лише як "угорський", а й як "алгоритм Куна–Манкреса" чи "алгоритм Манкреса".
Однак нещодавно, у 2006 році, було виявлено, що той самий алгоритм винайшов за століття до Куна німецький математик Карл Густав Якобі (Carl Gustav Jacobi). Його праця Про дослідження порядку системи довільних звичайних диференціальних рівнянь, опублікована посмертно у 1890 році, серед інших результатів містила поліноміальний алгоритм розв'язання задачі про призначення. На жаль, оскільки публікація була латиною, вона залишилася непоміченою серед математиків.
Варто також зазначити, що оригінальний алгоритм Куна мав асимптотичну складність , і лише пізніше Джек Едмондс (Jack Edmonds) і Річард Карп (Richard Karp) (а незалежно від них — Томідзава) показали, як покращити її до асимптотичної складності .
Алгоритм за
Щоб уникнути неоднозначності, відразу зауважимо, що ми переважно маємо справу із задачею про призначення в матричному формулюванні (тобто дано матрицю , і потрібно вибрати з неї комірок, що лежать у різних рядках і стовпцях). Масиви ми індексуємо з , тобто, наприклад, матриця має індекси .
Ми також вважатимемо, що всі числа в матриці A невід'ємні (якщо це не так, матрицю завжди можна зробити невід'ємною, додавши до всіх чисел деяку константу).
Назвемо потенціалом два довільні масиви чисел та такі, що виконується наступна умова:
(Як бачимо, відповідає -му рядку, а — -му стовпцю матриці).
Назвемо значенням потенціалу суму його елементів:
З одного боку, легко бачити, що вартість шуканого розв'язку не менша за значення будь-якого потенціалу.
Лема.
Доведення
Шуканий розв'язок задачі складається з комірок матриці , тому для кожної з них . Оскільки всі елементи в лежать у різних рядках і стовпцях, підсумовуючи ці нерівності по всіх вибраних , отримуємо у лівій частині нерівності та — у правій.
З іншого боку, виявляється, що завжди існують розв'язок і потенціал, які перетворюють цю нерівність на рівність. Описаний нижче угорський алгоритм буде конструктивним доведенням цього факту. Поки що звернімо лише увагу на те, що якщо вартість якогось розв'язку дорівнює якомусь потенціалу, то цей розв'язок є оптимальним.
Зафіксуємо деякий потенціал. Назвемо ребро жорстким, якщо
Пригадаємо альтернативне формулювання задачі про призначення з використанням двочасткового графа. Позначимо через двочастковий граф, складений лише з жорстких ребер. Для поточного потенціалу угорський алгоритм підтримуватиме парування з максимальною кількістю ребер у графі . Щойно міститиме ребер, розв'язком задачі буде саме (адже це буде розв'язок, вартість якого збігається зі значенням потенціалу).
Перейдімо безпосередньо до опису алгоритму.
Крок 1. На початку потенціал вважається нульовим ( для всіх ), а парування вважається порожнім.
Крок 2. Далі на кожному кроці алгоритму ми намагаємося, не змінюючи потенціалу, збільшити потужність поточного парування на одиницю (нагадаємо, що парування шукається в графі жорстких ребер ). Для цього використовується звичайний алгоритм Куна для пошуку максимального парування у двочасткових графах. Нагадаємо цей алгоритм тут. Усі ребра парування орієнтуються в напрямку від правої частини до лівої, а всі інші ребра графа — у протилежному напрямку.
Пригадаємо (з термінології пошуку парувань), що вершина називається насиченою, якщо до неї прилягає ребро поточного парування. Вершина, до якої не прилягає жодне ребро поточного парування, називається ненасиченою. Шлях непарної довжини, у якому перше ребро не належить паруванню, а для всіх наступних ребер належність до парування чергується (належить/не належить), називається доповнювальним шляхом. З усіх ненасичених вершин лівої частини запускається пошук у глибину або пошук у ширину. Якщо внаслідок пошуку вдалося дістатися до ненасиченої вершини правої частини, то ми знайшли доповнювальний шлях з лівої частини до правої. Якщо ми включимо до парування непарні ребра шляху й вилучимо парні (тобто включимо до парування перше ребро, виключимо друге, включимо третє і т. д.), то збільшимо потужність парування на одиницю.
Якщо доповнювального шляху не було, то поточне парування є максимальним у графі .
Крок 3. Якщо на поточному кроці не вдається збільшити потужність поточного парування, то виконується перерахунок потенціалу так, щоб на наступних кроках було більше можливостей для збільшення парування.
Позначимо через множину вершин лівої частини, які були відвідані під час останнього обходу алгоритму Куна, а через — множину відвіданих вершин правої частини.
Обчислимо величину :
Лема.
Доведення
Припустимо, що . Тоді існує жорстке ребро з та . Звідси випливає, що ребро має бути орієнтоване від правої частини до лівої, тобто має входити до парування . Однак це неможливо, бо ми не могли дістатися до насиченої вершини інакше, ніж пройшовши ребром від j до i. Отже, .
Тепер перерахуємо потенціал таким чином:
-
для всіх вершин робимо ,
-
для всіх вершин робимо .
Лема. Отриманий потенціал залишається коректним потенціалом.
Доведення
Покажемо, що після перерахунку для всіх . Для всіх елементів з та сума не змінюється, тому нерівність залишається істинною. Для всіх елементів з та сума зменшується на , тому нерівність так само залишається істинною. Для інших елементів, у яких та , сума зростає, але нерівність усе одно зберігається, оскільки величина за означенням є максимальним приростом, який не порушує нерівність.
Лема. Старе парування жорстких ребер залишається коректним, тобто всі ребра парування лишаться жорсткими.
Доведення
Щоб якесь жорстке ребро перестало бути жорстким унаслідок зміни потенціалу, потрібно, щоб рівність перетворилася на нерівність . Однак це може статися лише тоді, коли та . Але означає, що ребро не могло бути ребром парування.
Лема. Після кожного перерахунку потенціалу кількість вершин, досяжних обходом, тобто , строго зростає.
Доведення
По-перше, зауважимо, що будь-яка вершина, що була досяжною до перерахунку, лишається досяжною. Справді, якщо деяка вершина досяжна, то існує деякий шлях до неї з досяжних вершин, що починається з ненасиченої вершини лівої частини; оскільки для ребер виду сума не змінюється, весь цей шлях збережеться після зміни потенціалу. По-друге, покажемо, що після перерахунку стане досяжною щонайменше одна нова вершина. Це випливає з означення : ребро , до якого відноситься , стане жорстким, тож вершина стане досяжною з вершини .
Через останню лему до знаходження доповнювального шляху та збільшення потужності парування може статися не більше перерахунків потенціалу. Таким чином, рано чи пізно буде знайдено потенціал, що відповідає досконалому паруванню , і буде відповіддю до задачі. Якщо говорити про складність алгоритму, то вона становить : загалом має бути не більше збільшень парування, перед кожним з яких — не більше перерахунків потенціалу, кожен з яких виконується за час .
Ми не наводитимемо тут реалізацію алгоритму за , оскільки вона виявиться не коротшою за реалізацію алгоритму за , описану нижче.
Алгоритм за
Тепер навчімося реалізовувати той самий алгоритм за (для прямокутних задач — за ).
Ключова ідея полягає в тому, щоб розглядати рядки матриці по одному, а не всі одразу. Таким чином, описаний вище алгоритм набуде наступного вигляду:
-
Розглядаємо наступний рядок матриці .
-
Поки немає доповнювального шляху, що починається в цьому рядку, перераховуємо потенціал.
-
Щойно знайдено доповнювальний шлях, розповсюджуємо парування вздовж нього (тим самим включаючи останнє ребро до парування) і починаємо з кроку 1 заново (щоб розглянути наступний рядок).
Щоб досягти потрібної складності, необхідно реалізувати кроки 2–3, які виконуються для кожного рядка матриці, за час (для прямокутних задач — за ).
Для цього пригадаємо два доведені вище факти:
-
Зі зміною потенціалу вершини, що були досяжні обходом Куна, лишаться досяжними.
-
Загалом до знаходження доповнювального шляху могло статися лише перерахунків потенціалу.
Звідси випливають такі ключові ідеї, що дозволяють досягти потрібної складності:
-
Щоб перевірити наявність доповнювального шляху, немає потреби запускати обхід Куна заново після кожного перерахунку потенціалу. Натомість можна виконувати обхід Куна в ітеративній формі: після кожного перерахунку потенціалу дивимося на додані жорсткі ребра і, якщо їхні ліві кінці були досяжними, позначаємо їхні праві кінці теж досяжними та продовжуємо обхід з них.
-
Розвиваючи цю ідею далі, можемо подати алгоритм так: на кожному кроці циклу перераховується потенціал. Після цього визначається стовпець, що став досяжним (а такий завжди існуватиме, оскільки після кожного перерахунку потенціалу з'являються нові досяжні вершини). Якщо стовпець ненасичений, то знайдено доповнювальний ланцюг. І навпаки, якщо стовпець насичений, то досяжним стає й рядок парування.
-
Щоб швидко перераховувати потенціал (швидше за наївну версію за ), потрібно підтримувати допоміжні мінімуми для кожного зі стовпців:
Легко бачити, що шукана величина виражається через них так:
Таким чином, знаходження тепер можна виконати за .
Необхідно оновлювати масив , коли з'являються нові відвідані рядки. Це можна зробити за для доданого рядка (що сумарно по всіх рядках дає ). Також необхідно оновлювати масив при перерахунку потенціалу, що теж робиться за час ( змінюється лише для стовпців, які ще не були досягнуті: а саме, зменшується на ).
Таким чином, алгоритм набуває наступного вигляду: у зовнішньому циклі ми розглядаємо рядки матриці по одному. Кожен рядок обробляється за час , оскільки могло статися лише перерахунків потенціалу (кожен за час ), а масив підтримується за час ; алгоритм Куна працюватиме за час (оскільки він поданий у формі ітерацій, кожна з яких відвідує новий стовпець).
Отримана складність становить або, якщо задача прямокутна, .
Реалізація угорського алгоритму
Наведену нижче реалізацію розробив Андрій Лопатін кілька років тому. Вона вирізняється дивовижною лаконічністю: весь алгоритм складається з 30 рядків коду.
Реалізація знаходить розв'язок для прямокутної матриці , де . Матриця 1-індексована для зручності та стислості коду: ця реалізація вводить фіктивний нульовий рядок і нульовий стовпець, що дозволяє нам записувати багато циклів у загальному вигляді, без додаткових перевірок.
Масиви та зберігають потенціал. Спочатку вони встановлюються в нуль, що узгоджується з матрицею з нульових рядків (Зауважимо, що для цієї реалізації неважливо, чи містить матриця від'ємні числа).
Масив містить парування: для кожного стовпця він зберігає номер вибраного рядка (або , якщо ще нічого не вибрано). Для зручності реалізації вважається рівним номеру поточного рядка.
Масив містить для кожного стовпця допоміжні мінімуми, потрібні для швидкого перерахунку потенціалу, як описано вище.
Масив містить інформацію про те, де ці мінімуми досягаються, щоб ми могли пізніше відновити доповнювальний шлях. Зауважимо, що для відновлення шляху достатньо зберігати лише значення стовпців, оскільки номери рядків можна взяти з парування (тобто з масиву ). Таким чином, для кожного стовпця містить номер попереднього стовпця в шляху (або , якщо такого немає).
Сам алгоритм є зовнішнім циклом по рядках матриці, всередині якого розглядається -й рядок матриці. Перший цикл do-while виконується, доки не буде знайдено вільний стовпець . Кожна ітерація циклу позначає відвіданим новий стовпець з номером (обчислений на попередній ітерації; а спочатку рівний нулю — тобто ми починаємо з фіктивного стовпця), а також новий рядок — суміжний з ним у паруванні (тобто ; а спочатку, коли , береться -й рядок). Через появу нового відвіданого рядка потрібно відповідно перерахувати масив та . Якщо оновлюється, то стовпець стає мінімумом, який було досягнуто (зауважимо, що за такої реалізації може виявитися рівним нулю, що означає, що потенціал не можна змінити на поточному кроці: вже є новий досяжний стовпець). Після цього перераховуються потенціал та масив . Наприкінці циклу "do-while" ми знайшли доповнювальний шлях, що закінчується в стовпці , який можна "розгорнути" за допомогою масиву предків .
Константа INF — це "нескінченність", тобто деяке число, яке очевидно більше за всі можливі числа у вхідній матриці .
- C++
- Python
- TypeScript
- Go
vector<int> u (n+1), v (m+1), p (m+1), way (m+1);
for (int i=1; i<=n; ++i) {
p[0] = i;
int j0 = 0;
vector<int> minv (m+1, INF);
vector<bool> used (m+1, false);
do {
used[j0] = true;
int i0 = p[j0], delta = INF, j1;
for (int j=1; j<=m; ++j)
if (!used[j]) {
int cur = A[i0][j]-u[i0]-v[j];
if (cur < minv[j])
minv[j] = cur, way[j] = j0;
if (minv[j] < delta)
delta = minv[j], j1 = j;
}
for (int j=0; j<=m; ++j)
if (used[j])
u[p[j]] += delta, v[j] -= delta;
else
minv[j] -= delta;
j0 = j1;
} while (p[j0] != 0);
do {
int j1 = way[j0];
p[j0] = p[j1];
j0 = j1;
} while (j0);
}
# Матриця A 1-індексована: A[1..n][1..m], n <= m.
# Масиви u, v, p, way, minv, used також 1-індексовані (індекс 0 — фіктивний).
u = [0] * (n + 1)
v = [0] * (m + 1)
p = [0] * (m + 1)
way = [0] * (m + 1)
for i in range(1, n + 1):
p[0] = i
j0 = 0
minv = [INF] * (m + 1)
used = [False] * (m + 1)
while True: # do-while: шукаємо доповнювальний шлях
used[j0] = True
i0 = p[j0]
delta = INF
j1 = -1
for j in range(1, m + 1):
if not used[j]:
cur = A[i0][j] - u[i0] - v[j]
if cur < minv[j]:
minv[j] = cur
way[j] = j0
if minv[j] < delta:
delta = minv[j]
j1 = j
for j in range(0, m + 1): # перерахунок потенціалу та minv
if used[j]:
u[p[j]] += delta
v[j] -= delta
else:
minv[j] -= delta
j0 = j1
if p[j0] == 0:
break
while True: # розгортаємо доповнювальний шлях через way
j1 = way[j0]
p[j0] = p[j1]
j0 = j1
if j0 == 0:
break
// Матриця A 1-індексована: A[1..n][1..m], n <= m.
// Масиви u, v, p, way, minv, used теж 1-індексовані (індекс 0 — фіктивний).
const u = new Array<number>(n + 1).fill(0);
const v = new Array<number>(m + 1).fill(0);
const p = new Array<number>(m + 1).fill(0);
const way = new Array<number>(m + 1).fill(0);
for (let i = 1; i <= n; ++i) {
p[0] = i;
let j0 = 0;
const minv = new Array<number>(m + 1).fill(INF);
const used = new Array<boolean>(m + 1).fill(false);
do {
used[j0] = true;
const i0 = p[j0];
let delta = INF;
let j1 = -1;
for (let j = 1; j <= m; ++j) {
if (!used[j]) {
const cur = A[i0][j] - u[i0] - v[j];
if (cur < minv[j]) {
minv[j] = cur;
way[j] = j0;
}
if (minv[j] < delta) {
delta = minv[j];
j1 = j;
}
}
}
for (let j = 0; j <= m; ++j) {
if (used[j]) {
u[p[j]] += delta;
v[j] -= delta;
} else {
minv[j] -= delta;
}
}
j0 = j1;
} while (p[j0] !== 0);
do {
const j1 = way[j0];
p[j0] = p[j1];
j0 = j1;
} while (j0);
}
// Матриця A 1-індексована: A[1..n][1..m], n <= m.
// Зрізи u, v, p, way, minv, used теж 1-індексовані (індекс 0 — фіктивний).
u := make([]int, n+1)
v := make([]int, m+1)
p := make([]int, m+1)
way := make([]int, m+1)
for i := 1; i <= n; i++ {
p[0] = i
j0 := 0
minv := make([]int, m+1)
used := make([]bool, m+1)
for j := range minv {
minv[j] = INF
}
for { // do-while: шукаємо доповнювальний шлях
used[j0] = true
i0 := p[j0]
delta := INF
j1 := -1
for j := 1; j <= m; j++ {
if !used[j] {
cur := A[i0][j] - u[i0] - v[j]
if cur < minv[j] {
minv[j] = cur
way[j] = j0
}
if minv[j] < delta {
delta = minv[j]
j1 = j
}
}
}
for j := 0; j <= m; j++ { // перерахунок потенціалу та minv
if used[j] {
u[p[j]] += delta
v[j] -= delta
} else {
minv[j] -= delta
}
}
j0 = j1
if p[j0] == 0 {
break
}
}
for { // розгортаємо доповнювальний шлях через way
j1 := way[j0]
p[j0] = p[j1]
j0 = j1
if j0 == 0 {
break
}
}
}
Щоб відновити відповідь у більш звичному вигляді, тобто знайти для кожного рядка номер вибраного в ньому стовпця, можна зробити так:
- C++
- Python
- TypeScript
- Go
vector<int> ans (n+1);
for (int j=1; j<=m; ++j)
ans[p[j]] = j;
ans = [0] * (n + 1)
for j in range(1, m + 1):
ans[p[j]] = j
const ans = new Array<number>(n + 1).fill(0);
for (let j = 1; j <= m; ++j) ans[p[j]] = j;
ans := make([]int, n+1)
for j := 1; j <= m; j++ {
ans[p[j]] = j
}
Вартість парування можна просто взяти як потенціал нульового стовпця (взятий з протилежним знаком). Справді, як видно з коду, містить суму всіх значень , тобто сумарну зміну потенціалу. Хоча відразу могло змінитися кілька значень та , сумарна зміна потенціалу точно дорівнює , оскільки доки існує доповнювальний шлях, кількість досяжних рядків точно на один більша за кількість досяжних стовпців (лише поточний рядок не має "пари" у вигляді відвіданого стовпця):
- C++
- Python
- TypeScript
- Go
int cost = -v[0];
cost = -v[0]
const cost = -v[0];
cost := -v[0]
Зв'язок з алгоритмом послідовних найкоротших шляхів
Угорський алгоритм можна розглядати як алгоритм послідовних найкоротших шляхів, адаптований до задачі про призначення. Не вдаючись у деталі, дамо інтуїтивне розуміння зв'язку між ними.
Алгоритм послідовних шляхів використовує модифіковану версію алгоритму Джонсона як техніку перезважування. Вона ділиться на чотири кроки:
- Використовуємо алгоритм Беллмана–Форда, починаючи зі стоку , і для кожного вузла знаходимо мінімальну вагу шляху від до .
Для кожного кроку основного алгоритму:
- Перезважуємо ребра вихідного графа так: .
- Використовуємо алгоритм Дейкстри для пошуку підграфа найкоротших шляхів вихідної мережі.
- Оновлюємо потенціали для наступної ітерації.
З огляду на цей опис можна помітити сильну аналогію між та потенціалами: можна перевірити, що вони рівні з точністю до сталого зсуву. Крім того, можна показати, що після перезважування множина всіх ребер нульової ваги є підграфом найкоротших шляхів, у якому основний алгоритм намагається збільшити потік. Це саме відбувається і в угорському алгоритмі: ми створюємо підграф із жорстких ребер (тих, для яких величина дорівнює нулю) і намагаємося збільшити розмір парування.
На кроці 4 оновлюються всі : щоразу, коли ми змінюємо потокову мережу, ми маємо гарантувати, що відстані від джерела правильні (інакше на наступній ітерації алгоритм Дейкстри може дати збій). Це нагадує оновлення, яке виконується над потенціалами, але в цьому випадку вони збільшуються неоднаково.
Щоб глибше зрозуміти потенціали, зверніться до цієї статті.
Приклади задач
Ось кілька прикладів, пов'язаних із задачею про призначення, від зовсім тривіальних до менш очевидних:
-
Дано двочастковий граф, у якому потрібно знайти максимальне парування з мінімальною вагою (тобто, насамперед максимізується розмір парування, а вже потім мінімізується його вартість).
Щоб розв'язати її, ми просто будуємо задачу про призначення, ставлячи число "нескінченність" на місце відсутніх ребер. Після цього розв'язуємо задачу угорським алгоритмом і вилучаємо з відповіді ребра нескінченної ваги (вони могли потрапити у відповідь, якщо задача не має розв'язку у вигляді досконалого парування). -
Дано двочастковий граф, у якому потрібно знайти максимальне парування з максимальною вагою.
Розв'язок знову очевидний: усі ваги треба помножити на мінус одиницю. -
Задача виявлення рухомих об'єктів на зображеннях: було зроблено два знімки, унаслідок чого отримано дві множини координат. Потрібно співвіднести об'єкти на першому й другому зображеннях, тобто визначити для кожної точки другого зображення, якій точці першого зображення вона відповідала. При цьому потрібно мінімізувати суму відстаней між зіставленими точками (тобто ми шукаємо розв'язок, у якому об'єкти сумарно подолали найкоротший шлях).
Для розв'язання ми просто будуємо й розв'язуємо задачу про призначення, де вагами ребер є евклідові відстані між точками. -
Задача виявлення рухомих об'єктів локаторами: є два локатори, які не можуть визначити положення об'єкта в просторі, а лише його напрямок. Обидва локатори (розташовані в різних точках) отримали інформацію у вигляді таких напрямків. Потрібно визначити положення об'єктів, тобто визначити очікувані положення об'єктів та відповідні їм пари напрямків так, щоб сума відстаней від об'єктів до променів напрямків була мінімальною.
Розв'язок: знову ж таки, ми просто будуємо й розв'язуємо задачу про призначення, де вершинами лівої частини є напрямків від першого локатора, вершинами правої частини — напрямків від другого локатора, а вагами ребер — відстані між відповідними променями. -
Покриття орієнтованого ациклічного графа шляхами: дано орієнтований ациклічний граф, у якому потрібно знайти найменшу кількість шляхів (за рівності — з найменшою сумарною вагою) так, щоб кожна вершина графа лежала рівно в одному шляху.
Розв'язок полягає в тому, щоб побудувати з даного графа відповідний двочастковий граф і знайти в ньому максимальне парування мінімальної ваги. Докладніше див. окрему статтю. -
Розфарбовування дерева. Дано дерево, у якому кожна вершина, окрім листків, має рівно дітей. Потрібно вибрати для кожної вершини один із доступних кольорів так, щоб жодні дві суміжні вершини не мали однакового кольору. Крім того, для кожної вершини та кожного кольору відома вартість фарбування цієї вершини цим кольором, і потрібно мінімізувати сумарну вартість.
Щоб розв'язати цю задачу, скористаємося динамічним програмуванням. А саме, навчімося обчислювати величину , де — номер вершини, — номер кольору, а сама величина — це мінімальна вартість, потрібна для розфарбовування всіх вершин у піддереві з коренем у , причому саму вершину — кольором . Щоб обчислити таку величину , необхідно розподілити решту кольорів між дітьми вершини , а для цього необхідно побудувати й розв'язати задачу про призначення (у якій вершинами лівої частини є кольори, вершинами правої частини — діти, а вагами ребер — відповідні значення ).
Таким чином, кожне значення обчислюється за допомогою розв'язку задачі про призначення, що зрештою дає асимптотику . -
Якщо в задачі про призначення ваги знаходяться не на ребрах, а на вершинах, причому лише на вершинах однієї частини, то немає потреби застосовувати угорський алгоритм: достатньо відсортувати вершини за вагою й запустити звичайний алгоритм Куна.
-
Розглянемо наступний окремий випадок. Нехай кожній вершині лівої частини призначено деяке число , а кожній вершині правої частини — . Нехай вага будь-якого ребра дорівнює (числа та відомі). Розв'яжіть задачу про призначення.
Щоб розв'язати її без угорського алгоритму, спочатку розглянемо випадок, коли обидві частини мають по дві вершини. У цьому випадку, як легко бачити, краще з'єднувати вершини у зворотному порядку: з'єднати вершину з меншим з вершиною з більшим . Це правило легко узагальнюється на довільну кількість вершин: потрібно відсортувати вершини першої частини в порядку зростання значень , другої частини — в порядку спадання значень , і з'єднати вершини попарно в цьому порядку. Таким чином, ми отримуємо розв'язок зі складністю . -
Задача про потенціали. Дано матрицю , потрібно знайти два масиви та такі, що для будь-яких та виконується , а сума елементів масивів та є максимальною.
Знаючи угорський алгоритм, розв'язати цю задачу буде неважко: угорський алгоритм якраз і знаходить такий потенціал , що задовольняє умову задачі. З іншого боку, без знання угорського алгоритму розв'язати таку задачу видається майже неможливим.ЗауваженняЦю задачу також називають двоїстою задачею до задачі про призначення: мінімізація сумарної вартості призначення еквівалентна максимізації суми потенціалів.
Література
-
Ravindra Ahuja, Thomas Magnanti, James Orlin. Network Flows [1993]
-
Harold Kuhn. The Hungarian Method for the Assignment Problem [1955]
-
James Munkres. Algorithms for Assignment and Transportation Problems [1957]