Венгерский алгоритм решения задачи о назначениях — различия между версиями
Da1s111 (обсуждение | вклад) м (→Реализация) |
м (rollbackEdits.php mass rollback) |
||
(не показано 9 промежуточных версий 3 участников) | |||
Строка 58: | Строка 58: | ||
Алгоритм, решающий задачу, работает с графом, как с матрицей весов. | Алгоритм, решающий задачу, работает с графом, как с матрицей весов. | ||
− | + | * Вычитаем из каждой строки значение ее минимального элемента. Теперь в каждой строке есть хотя бы один нулевой элемент. | |
− | + | * Вычитаем из каждого столбца значение его минимального элемента. Теперь в каждом столбце есть хотя бы один нулевой элемент. | |
− | + | * Ищем в текущем графе полное паросочетание из ребер нулевого веса: | |
− | + | ** Если оно найдено, то желаемый результат достигнут, алгоритм закончен. | |
− | + | ** В противном случае, покроем нули матрицы весов минимальным количеством строк и столбцов (это не что иное, как [[Связь максимального паросочетания и минимального вершинного покрытия в двудольных графах|нахождение минимального вершинного покрытия в двудольном графе]]). Пусть <tex> X_c </tex> и <tex> Y_c </tex> — множества вершин минимального вершинного покрытия из левой и правой долей (то есть, строк и столбцов) соответственно, тогда применим преобразование <tex> X_c \uparrow\downarrow (Y \setminus Y_c) </tex>. Для этого преобразования <tex> d </tex> будет минимумом по всем ребрам между <tex> X \setminus X_c </tex> и <tex> Y \setminus Y_c </tex>, то есть, ребер нулевого веса здесь нет, поэтому, после его выполнения в матрице весов появится новый нуль. После этого перейдем к шагу 1. | |
− | |||
== Анализ времени работы == | == Анализ времени работы == | ||
Строка 77: | Строка 76: | ||
* Начало | * Начало | ||
* '''Шаг 0.''' Введем ''следующее понятие'': | * '''Шаг 0.''' Введем ''следующее понятие'': | ||
− | + | : Назовём потенциалом два произвольных массива чисел <tex> u[1 \ldots n] </tex> и <tex> v[1 \ldots n] </tex> таких, что выполняется условие: | |
− | + | <center> <tex> u[i] + v[j] \leqslant a[i][j] ~ (i = 1 \ldots n)</tex>, где <tex> a </tex> {{---}} заданная матрица. </center> | |
− | * '''Шаг 1.''' Добавляем в рассмотрение очередную строку матрицы <tex> a </tex> | + | * '''Шаг 1.''' Добавляем в рассмотрение очередную строку матрицы <tex> a. </tex> |
* '''Шаг 2.''' Пока нет увеличивающей цепи, начинающейся в этой строке, пересчитываем потенциал. | * '''Шаг 2.''' Пока нет увеличивающей цепи, начинающейся в этой строке, пересчитываем потенциал. | ||
* '''Шаг 3.''' Как только появляется увеличивающая цепь, чередуем паросочетание вдоль неё (включая тем самым последнюю строку в паросочетание), и переходим к началу (к рассмотрению следующей строки). | * '''Шаг 3.''' Как только появляется увеличивающая цепь, чередуем паросочетание вдоль неё (включая тем самым последнюю строку в паросочетание), и переходим к началу (к рассмотрению следующей строки). | ||
* Конец | * Конец | ||
+ | |||
+ | === Ключевые идеи === | ||
+ | * Для проверки наличия увеличивающей цепочки нет необходимости запускать обход Куна заново после каждого пересчёта потенциала. Вместо этого можно оформить [[Алгоритм Куна для поиска максимального паросочетания|обход Куна]] в итеративном виде: после каждого пересчёта потенциала мы просматриваем добавившиеся жёсткие рёбра и, если их левые концы были достижимыми, помечаем их правые концы также как достижимые и продолжаем обход из них. | ||
+ | * Развивая эту идею дальше, можно прийти к такому представлению алгоритма: это цикл, на каждом шаге которого сначала пересчитывается потенциал, затем находится столбец, ставший достижимым (а таковой всегда найдётся, поскольку после пересчёта потенциала всегда появляются новые достижимые вершины), и если этот столбец был ненасыщен, то найдена увеличивающая цепь, а если столбец был насыщен — то соответствующая ему в паросочетании строка также становится достижимой. | ||
+ | * Теперь алгоритм принимает вид: цикл добавления столбцов, на каждом из которых сначала пересчитывается потенциал, а затем какой-то новый столбец помечается как достижимый. | ||
+ | * Чтобы быстро пересчитывать потенциал (быстрее, чем наивный вариант за <tex> O(n^2) </tex>), надо поддерживать вспомогательные минимумы по каждому из столбцов <tex> j </tex>. | ||
=== Реализация === | === Реализация === | ||
Строка 88: | Строка 93: | ||
<tex> \mathtt{a[1 \dots n][1 \dots m]} </tex> {{---}} прямоугольная входная матрица, где <tex> \mathtt{n \leqslant m} </tex>. Матрица хранится в 1-индексации. | <tex> \mathtt{a[1 \dots n][1 \dots m]} </tex> {{---}} прямоугольная входная матрица, где <tex> \mathtt{n \leqslant m} </tex>. Матрица хранится в 1-индексации. | ||
− | <tex> \mathtt{u[0 \dots n], v[0 \dots n]} </tex> {{---}} потенциал. | + | <tex> \mathtt{u[0 \dots n], ~ v[0 \dots n]} </tex> {{---}} потенциал. |
<tex> \mathtt{p[0 \dots m]} </tex> {{---}} массив паросочетания. Для каждого стобца <tex> \mathtt{i = 0 \dots m} </tex> он хранит номер соответствующей выбранной строки <tex> \mathtt{p[i]} </tex> (или <tex> \mathtt{0} </tex>, если ничего не выбрано). Полагаем, что <tex> \mathtt{p[0]} </tex> равно номеру рассматриваемой строки. | <tex> \mathtt{p[0 \dots m]} </tex> {{---}} массив паросочетания. Для каждого стобца <tex> \mathtt{i = 0 \dots m} </tex> он хранит номер соответствующей выбранной строки <tex> \mathtt{p[i]} </tex> (или <tex> \mathtt{0} </tex>, если ничего не выбрано). Полагаем, что <tex> \mathtt{p[0]} </tex> равно номеру рассматриваемой строки. | ||
− | <tex> \mathtt{minv[1 \dots m]} </tex> {{---}} массив, хранящий для каждого столбца j вспомогательные минимумы, необходимые для быстрого пересчета потенциала. | + | <tex> \mathtt{minv[1 \dots m]} </tex> {{---}} массив, хранящий для каждого столбца <tex> \mathtt{j} </tex> вспомогательные минимумы, необходимые для быстрого пересчета потенциала. |
− | <tex> \mathtt{minv[j] = \min\limits_{i \in Z_1}(a[i][j] - u[i] - v[j])} </tex> | + | <tex> \mathtt{minv[j] = \min\limits_{i \in Z_1}(a[i][j] - u[i] - v[j])} </tex>, где <tex> \mathtt{Z_1} </tex> {{---}} множество вершин первой доли, которые были посещены обходом [[Алгоритм Куна для поиска максимального паросочетания|алгоритма Куна]] при попытке поиска увеличивающей цепи. |
<tex> \mathtt{way[1 \dots m]} </tex> {{---}} массив, содержащий информацию о том, где эти минимумы достигаются, чтобы мы могли впоследствии восстановить [[Паросочетания: основные определения, теорема о максимальном паросочетании и дополняющих цепях|увеличивающую цепочку]]. | <tex> \mathtt{way[1 \dots m]} </tex> {{---}} массив, содержащий информацию о том, где эти минимумы достигаются, чтобы мы могли впоследствии восстановить [[Паросочетания: основные определения, теорема о максимальном паросочетании и дополняющих цепях|увеличивающую цепочку]]. | ||
'''function''' hungarianAlgorithm(a): | '''function''' hungarianAlgorithm(a): | ||
− | '''for''' i = 1 '''to''' n // рассматриваем строки матрицы ''a'' | + | '''for''' i = 1 '''to''' n <font color=darkgreen>// рассматриваем строки матрицы ''a'' </font> |
+ | p[0] = i <font color=darkgreen>// для удобства реализации </font> | ||
+ | j0 = 0 <font color=darkgreen>// свободный столбец </font> | ||
заполняем массивы ''minv'' {{---}} <tex> \infty </tex>, ''used'' {{---}} ''false'' | заполняем массивы ''minv'' {{---}} <tex> \infty </tex>, ''used'' {{---}} ''false'' | ||
− | '''while''' ''true'' // ищем свободный столбец | + | '''while''' ''true'' <font color=darkgreen>// ищем свободный столбец </font> |
− | помечаем посещенными столбец ''j0'' и строку ''i0'' | + | used[j0] = ''true'', i0 = p[j0] <font color=darkgreen>// помечаем посещенными столбец ''j0'' и строку ''i0'' </font> |
− | пересчитываем массив ''minv'', находим в нем минимум ''<tex> \delta </tex>'' и столбец ''j1'', в котором он достигнут | + | пересчитываем массив ''minv'', находим в нем минимум ''<tex> \delta </tex>'' (изначально ''<tex> \infty </tex>'') и столбец ''j1'', в котором он достигнут |
− | производим пересчет потенциала ''u'' и ''v'', соответствующее изменение ''minv'' | + | '''for''' j = 0 '''to''' m <font color=darkgreen>// производим пересчет потенциала ''u'' и ''v'', соответствующее изменение ''minv'' </font> |
+ | '''if''' used[j] | ||
+ | u[p[j]] += <tex> \delta </tex> | ||
+ | v[j] -= <tex> \delta </tex> | ||
+ | '''else''' | ||
+ | minv[j] -= <tex> \delta </tex> | ||
если нашли свободный столбец {{---}} выходим из цикла | если нашли свободный столбец {{---}} выходим из цикла | ||
ищем увеличивающуюся цепочку, пользуясь массивом предков ''way'' | ищем увеличивающуюся цепочку, пользуясь массивом предков ''way'' | ||
=== Время работы === | === Время работы === | ||
− | Оценим время работы алгоритма. Во внешнем цикле мы добавляем в рассмотрение строки матрицы одну за другой. Каждая строка обрабатывается за время <tex> O(n^2) </tex>, поскольку при этом могло происходить лишь <tex> O(n) </tex> пересчётов потенциала (каждый — за время <tex> O(n) </tex>), для чего за время <tex> O(n^2) </tex> поддерживается массив <tex> | + | Оценим время работы алгоритма. Во внешнем цикле мы добавляем в рассмотрение строки матрицы одну за другой. Каждая строка обрабатывается за время <tex> O(n^2) </tex>, поскольку при этом могло происходить лишь <tex> O(n) </tex> пересчётов потенциала (каждый — за время <tex> O(n) </tex>), для чего за время <tex> O(n^2) </tex> поддерживается массив <tex> minv </tex>; [[Алгоритм Куна для поиска максимального паросочетания|алгоритм Куна]] суммарно отработает за время <tex> O(n^2) </tex> (поскольку он представлен в форме <tex> O(n) </tex> итераций, на каждой из которых посещается новый столбец). |
Итоговая асимптотика составляет <tex> O(n^3) </tex>. | Итоговая асимптотика составляет <tex> O(n^3) </tex>. |
Текущая версия на 19:21, 4 сентября 2022
Венгерский алгоритм (англ. Hungarian algorithm) — алгоритм, решающий задачу о назначениях за полиномиальное время. Оригинальная версия была придумана и разработана Х. Куном в 1955 году и имела асимптотику
, но позже Эдмонс и Карп (а также, независимо от них, Томидзава) показали, что можно улучшить ее до .Задача: |
Пусть дан взвешенный полный двудольный граф c целыми весами ребер , нужно найти в нем полное паросочетание минимального веса. Вес паросочетания определяется как сумма весов его ребер. Далее будем обозначать левую и правую доли графа за и соответственно, вес ребра — как . |
Содержание
Вспомогательные леммы
Лемма: |
Если веса всех ребер графа, инцидентных какой-либо вершине, изменить (увеличить или уменьшить) на одно и то же число, то в новом графе оптимальное паросочетание будет состоять из тех же ребер, что и в старом. |
Доказательство: |
Полное паросочетание для каждой вершины содержит ровно одно ребро, инцидентное этой вершине. Указанная операция изменит на одно и то же число вес любого паросочетания. Значит, ребро, которое принадлежало оптимальному паросочетанию в старом графе, в новом графе тоже будет ему принадлежать. |
Далее будем рассматривать только графы с неотрицательной весовой функцией, так как, согласно этой лемме, задачу о назначениях на остальных графах можно свести к задаче о назначениях на них.
Лемма: | |||||||||
Выделим в множествах и подмножества . Пусть . Прибавим ко всем весам ребер, инцидентных вершинам из . Затем отнимем от всех весов ребер, инцидентных вершинам из (далее для краткости эта операция обозначается как ). Тогда:
| |||||||||
Доказательство: | |||||||||
Рассмотрим матрицу весов графа. Не умаляя общности, можно сказать, что множества и состоят из первых элементов множеств и соответственно (мы упорядочиваем множества по номерам вершин). Тогда вся матрица делится на 4 блока: | |||||||||
Лемма: |
Если веса всех ребер графа неотрицательны и некоторое полное паросочетание состоит из ребер нулевого веса, то оно является оптимальным. |
Доказательство: |
Действительно, паросочетание с какими-то другими весами ребер имеет больший вес и оптимальным не является. |
Общий метод
Доказанные ранее утверждения позволяют придумать схему алгоритма, решающего задачу о назначениях: нужно найти полное паросочетание из ребер нулевого веса в графе, полученном из исходного преобразованиями, описанными в первых двух леммах.
Алгоритм, решающий задачу, работает с графом, как с матрицей весов.
- Вычитаем из каждой строки значение ее минимального элемента. Теперь в каждой строке есть хотя бы один нулевой элемент.
- Вычитаем из каждого столбца значение его минимального элемента. Теперь в каждом столбце есть хотя бы один нулевой элемент.
- Ищем в текущем графе полное паросочетание из ребер нулевого веса:
- Если оно найдено, то желаемый результат достигнут, алгоритм закончен.
- В противном случае, покроем нули матрицы весов минимальным количеством строк и столбцов (это не что иное, как нахождение минимального вершинного покрытия в двудольном графе). Пусть и — множества вершин минимального вершинного покрытия из левой и правой долей (то есть, строк и столбцов) соответственно, тогда применим преобразование . Для этого преобразования будет минимумом по всем ребрам между и , то есть, ребер нулевого веса здесь нет, поэтому, после его выполнения в матрице весов появится новый нуль. После этого перейдем к шагу 1.
Анализ времени работы
Поиск максимального паросочетания или минимального вершинного покрытия в двудольном графе совершается за операций. При каждом повторении шагов 1-4 в матрице весов появляется новый нуль. Этот нуль соответствует некоторому новому ребру между вершинами из множеств и . Всего в графе ребер, значит, всего будет совершено не более итераций внешнего цикла. Поэтому, верхняя оценка времени работы данного метода — . Более точная оценка довольно сложна и зависит от порядка чисел в матрице весов графа.
Алгоритм за
Общая идея
Будем добавлять в рассмотрение строки матрицы одну за одной, а не рассматривать их все сразу.
Описание алгоритма
- Начало
- Шаг 0. Введем следующее понятие:
- Назовём потенциалом два произвольных массива чисел и таких, что выполняется условие:
- Шаг 1. Добавляем в рассмотрение очередную строку матрицы
- Шаг 2. Пока нет увеличивающей цепи, начинающейся в этой строке, пересчитываем потенциал.
- Шаг 3. Как только появляется увеличивающая цепь, чередуем паросочетание вдоль неё (включая тем самым последнюю строку в паросочетание), и переходим к началу (к рассмотрению следующей строки).
- Конец
Ключевые идеи
- Для проверки наличия увеличивающей цепочки нет необходимости запускать обход Куна заново после каждого пересчёта потенциала. Вместо этого можно оформить обход Куна в итеративном виде: после каждого пересчёта потенциала мы просматриваем добавившиеся жёсткие рёбра и, если их левые концы были достижимыми, помечаем их правые концы также как достижимые и продолжаем обход из них.
- Развивая эту идею дальше, можно прийти к такому представлению алгоритма: это цикл, на каждом шаге которого сначала пересчитывается потенциал, затем находится столбец, ставший достижимым (а таковой всегда найдётся, поскольку после пересчёта потенциала всегда появляются новые достижимые вершины), и если этот столбец был ненасыщен, то найдена увеличивающая цепь, а если столбец был насыщен — то соответствующая ему в паросочетании строка также становится достижимой.
- Теперь алгоритм принимает вид: цикл добавления столбцов, на каждом из которых сначала пересчитывается потенциал, а затем какой-то новый столбец помечается как достижимый.
- Чтобы быстро пересчитывать потенциал (быстрее, чем наивный вариант за ), надо поддерживать вспомогательные минимумы по каждому из столбцов .
Реализация
— прямоугольная входная матрица, где . Матрица хранится в 1-индексации.
— потенциал.
— массив паросочетания. Для каждого стобца он хранит номер соответствующей выбранной строки (или , если ничего не выбрано). Полагаем, что равно номеру рассматриваемой строки.
— массив, хранящий для каждого столбца вспомогательные минимумы, необходимые для быстрого пересчета потенциала.
алгоритма Куна при попытке поиска увеличивающей цепи.
, где — множество вершин первой доли, которые были посещены обходом — массив, содержащий информацию о том, где эти минимумы достигаются, чтобы мы могли впоследствии восстановитьfunction hungarianAlgorithm(a): for i = 1 to n // рассматриваем строки матрицы a p[0] = i // для удобства реализации j0 = 0 // свободный столбец заполняем массивы minv —, used — false while true // ищем свободный столбец used[j0] = true, i0 = p[j0] // помечаем посещенными столбец j0 и строку i0 пересчитываем массив minv, находим в нем минимум (изначально ) и столбец j1, в котором он достигнут for j = 0 to m // производим пересчет потенциала u и v, соответствующее изменение minv if used[j] u[p[j]] += v[j] -= else minv[j] -= если нашли свободный столбец — выходим из цикла ищем увеличивающуюся цепочку, пользуясь массивом предков way
Время работы
Оценим время работы алгоритма. Во внешнем цикле мы добавляем в рассмотрение строки матрицы одну за другой. Каждая строка обрабатывается за время алгоритм Куна суммарно отработает за время (поскольку он представлен в форме итераций, на каждой из которых посещается новый столбец).
, поскольку при этом могло происходить лишь пересчётов потенциала (каждый — за время ), для чего за время поддерживается массив ;Итоговая асимптотика составляет
.См. также
- Алгоритм Куна для поиска максимального паросочетания
- Связь максимального паросочетания и минимального вершинного покрытия в двудольных графах
Источники информации
- Асанов М., Баранский В., Расин В. — Дискретная математика: Графы, матроиды, алгоритмы — 2010, 368 стр.
- Венгерский алготитм в Википедии
- Визуализатор алгоритма
- Реализация венгерского алгоритма на C++
- Венгерский алгоритм решения задачи о назначениях