Венгерский метод — это алгоритм комбинаторной оптимизации , который решает задачу назначения за полиномиальное время и который предвосхитил более поздние методы прямого и двойственного типа . Он был разработан и опубликован в 1955 году Гарольдом Куном , который дал ему название «Венгерский метод», поскольку алгоритм в значительной степени основывался на более ранних работах двух венгерских математиков, Денеша Кёнига и Йенё Эгервари . [1] [2] Однако в 2006 году было обнаружено, что Карл Густав Якоби решил задачу назначения в 19 веке, и решение было опубликовано посмертно в 1890 году на латыни. [3]
Джеймс Манкрес рассмотрел алгоритм в 1957 году и заметил, что он (сильно) полиномиален . [4] С тех пор алгоритм также известен как алгоритм Куна–Мункреса или алгоритм назначения Манкреса . Временная сложность исходного алгоритма была , однако Эдмондс и Карп , и независимо Томизава заметили, что его можно модифицировать для достижения времени выполнения. [5] [6] Форд и Фулкерсон распространили метод на общие задачи максимального потока в форме алгоритма Форда–Фулкерсона .
В этом простом примере есть три работника: Элис, Боб и Кэрол. Один из них должен убрать ванную, другой подмести полы, а третий помыть окна, но каждый из них требует разную оплату за разные задачи. Проблема заключается в том, чтобы найти наиболее дешевый способ распределения работ. Проблему можно представить в виде матрицы затрат на работников, выполняющих работы. Например:
Венгерский метод, примененный к таблице выше, даст минимальную стоимость: это $15, что достигается за счет того, что Алиса убирает ванную, Кэрол подметает полы, а Боб моет окна. Это можно подтвердить с помощью грубой силы:
В матричной формулировке нам дана матрица n × n , где элемент в i -й строке и j -м столбце представляет стоимость назначения j -й работы i -му работнику. Нам нужно найти назначение работ работникам, так чтобы каждая работа была назначена одному работнику, а каждому работнику была назначена одна работа, так что общая стоимость назначения была бы минимальной.
Это можно выразить как перестановку строк матрицы стоимости C для минимизации следа матрицы,
где P — матрица перестановки . (Эквивалентно, столбцы можно переставлять с помощью CP .)
Если цель состоит в том, чтобы найти задание, которое дает максимальную стоимость, задачу можно решить, отрицая матрицу стоимости C.
Алгоритм можно эквивалентно описать, сформулировав задачу с использованием двудольного графа. У нас есть полный двудольный граф с n вершинами рабочих ( S ) и n вершинами заданий ( T ), а каждое ребро ( E ) имеет стоимость . Мы хотим найти идеальное паросочетание с минимальной общей стоимостью.
Назовем функцию потенциалом , если для каждого .
Значение потенциала y равно сумме потенциалов по всем вершинам:
Стоимость каждого идеального паросочетания не меньше значения каждого потенциала: общая стоимость паросочетания не меньше суммы стоимостей всех ребер; стоимость каждого ребра не меньше суммы потенциалов его конечных точек; поскольку паросочетание идеальное, каждая вершина является конечной точкой ровно одного ребра; следовательно, общая стоимость не меньше общего потенциала.
Венгерский метод находит идеальное соответствие и потенциал, такие, что стоимость соответствия равна потенциальному значению. Это доказывает, что оба они оптимальны. Фактически, венгерский метод находит идеальное соответствие тесных ребер : ребро называется тесным для потенциала y , если . Обозначим подграф тесных ребер через . Стоимость идеального соответствия в (если оно есть) равна значению y .
В ходе алгоритма мы поддерживаем потенциал y и ориентацию (обозначаемую ) , которая обладает свойством, что ребра, ориентированные от T к S, образуют паросочетание M . Изначально y везде равен 0, и все ребра ориентированы от S к T (поэтому M пусто). На каждом шаге мы либо изменяем y так, чтобы его значение увеличивалось, либо изменяем ориентацию, чтобы получить паросочетание с большим количеством ребер. Мы сохраняем инвариант, что все ребра M являются плотными. Мы закончили, если M является идеальным паросочетанием.
В общем случае пусть и будут вершинами, не покрытыми M (то есть состоят из вершин в S без входящих ребер и состоят из вершин в T без исходящих ребер). Пусть Z будет множеством вершин, достижимых в из по направленному пути. Это можно вычислить с помощью поиска в ширину .
Если непусто, то меняем ориентацию всех ребер вдоль направленного пути в от до . Таким образом, размер соответствующего паросочетания увеличивается на 1.
Если пусто, то пусть
Δ хорошо определено, потому что по крайней мере одно такое ребро должно существовать всякий раз, когда соответствие еще не имеет максимально возможного размера (см. следующий раздел); оно положительно, потому что нет узких ребер между и . Увеличим y на Δ на вершинах и уменьшим y на Δ на вершинах . Результирующий y все еще является потенциалом, и хотя граф изменяется, он все еще содержит M (см. следующие подразделы). Мы ориентируем новые ребра от S до T . По определению Δ множество Z вершин, достижимых из , увеличивается (обратите внимание, что количество узких ребер не обязательно увеличивается).
Мы повторяем эти шаги до тех пор, пока M не станет идеальным соответствием, в этом случае он дает назначение минимальной стоимости. Время выполнения этой версии метода составляет : M увеличивается n раз, и в фазе, где M не изменяется, существует не более n потенциальных изменений (поскольку Z увеличивается каждый раз). Время, достаточное для потенциального изменения, составляет .
Мы должны показать, что пока соответствие не имеет максимально возможного размера, алгоритм всегда способен добиться прогресса — то есть либо увеличить количество сопоставленных ребер, либо сжать хотя бы одно ребро. Достаточно показать, что на каждом шаге выполняется хотя бы одно из следующих условий:
Если M имеет максимально возможный размер, то мы, конечно, закончили. В противном случае, по лемме Бержа , должен существовать увеличивающий путь P относительно M в базовом графе G. Однако этот путь может не существовать в : Хотя каждое четное ребро в P является плотным по определению M , нечетные ребра могут быть свободными и, таким образом, отсутствовать в . Одна конечная точка P находится в , другая в ; в логарифмическом отношении предположим, что он начинается в . Если каждое ребро на P является плотным, то оно остается увеличивающим путем в , и мы закончили. В противном случае пусть будет первым свободным ребром на P . Если то мы нашли путь со свободным хвостом, и мы закончили. В противном случае v достижим из некоторого другого пути Q плотных ребер из вершины в . Пусть будет подпутем P , начинающимся в v и продолжающимся до конца, и пусть будет путем, образованным путем перемещения по Q до достижения вершины на , а затем продолжающимся до конца . Заметим, что — увеличивающий путь в G с по крайней мере одним свободным ребром меньше, чем у P. P можно заменить на и этот процесс рассуждений можно повторять (формально, используя индукцию по числу свободных ребер), пока не будет найден увеличивающий путь или путь с свободным хвостом в G.
Чтобы показать, что каждое ребро в M остается после корректировки y , достаточно показать, что для произвольного ребра в M либо обе его конечные точки, либо ни одна из них не находятся в Z . Для этого пусть будет ребром в M из T в S . Легко видеть, что если v находится в Z , то и u тоже должно быть, так как каждое ребро в M является плотным. Теперь предположим, от противного, что но . Само u не может находиться в , так как оно является конечной точкой сопряженного ребра, поэтому должен быть некоторый направленный путь плотных ребер от вершины в к u . Этот путь должен избегать v , так как по предположению она не находится в Z , поэтому вершина, непосредственно предшествующая u в этом пути, является некоторой другой вершиной . является плотным ребром из T в S и, таким образом, находится в M . Но тогда M содержит два ребра, которые разделяют вершину u , что противоречит тому факту, что M является паросочетанием. Таким образом, каждое ребро в M имеет либо обе конечные точки, либо ни одну из конечных точек в Z .
Чтобы показать, что y остается потенциалом после корректировки, достаточно показать, что ни одно ребро не имеет общего потенциала, увеличенного сверх его стоимости. Это уже установлено для ребер в M предыдущим абзацем, поэтому рассмотрим произвольное ребро uv от S до T . Если увеличивается на Δ , то либо , в этом случае уменьшается на Δ , оставляя общий потенциал ребра неизменным, либо , в этом случае определение Δ гарантирует, что . Таким образом, y остается потенциалом.
Предположим, что есть задания и работники ( ). Мы описываем, как вычислить для каждого префикса заданий минимальную общую стоимость назначения каждого из этих заданий отдельным работникам. В частности, мы добавляем th задание и обновляем общую стоимость за время , получая общую временную сложность . Обратите внимание, что это лучше, чем когда количество заданий мало относительно количества работников.
Мы используем те же обозначения, что и в предыдущем разделе, хотя при необходимости изменяем их определения. Пусть обозначает множество первых рабочих мест, а обозначает множество всех рабочих.
Перед шагом th алгоритма мы предполагаем, что у нас есть сопоставление на , которое соответствует всем работам в и потенциалы, удовлетворяющие следующему условию: сопоставление является тесным относительно потенциалов, и потенциалы всех несопоставленных работников равны нулю, а потенциалы всех сопоставленных работников неположительны. Обратите внимание, что такие потенциалы подтверждают оптимальность сопоставления.
На шаге th мы добавляем th-ю работу в , чтобы сформировать и инициализировать . В любое время каждая вершина в будет достижима из th-й работы в . Пока не содержит работника, которому не назначена работа, пусть
и обозначают любой , при котором достигается минимум. После настройки потенциалов способом, описанным в предыдущем разделе, теперь есть узкий край от до .
Настройка потенциалов требует времени. Пересчет и после изменения потенциалов и также может быть выполнен за время. Случай 1 может произойти в большинстве случаев до того, как произойдет случай 2 и процедура завершится, что дает общую временную сложность .
Для удобства реализации код ниже добавляет дополнительного работника , который сохраняет отрицание суммы всех вычисленных до сих пор. После добавления th-го задания и обновления сопоставления стоимость текущего сопоставления становится равной сумме всех вычисленных до сих пор, или .
Этот код адаптирован из e-maxx :: algo. [7]
/*** Решение https://open.kattis.com/problems/cordonbleu с использованием венгерского языка* алгоритм.*/#include <кассета> #include <iostream> #include <лимиты> #включить <вектор> с использованием пространства имен std ; /*** Устанавливает a = min(a, b)* @return true, если b < a*/шаблон < класс T > bool ckmin ( T & a , const T & b ) { return b < a ? a = b , 1 : 0 ; } /*** При наличии J рабочих мест и W работников (J <= W) вычисляется минимальная стоимость назначения каждого* префикс рабочих мест для отдельных работников.** @tparam T — тип, достаточно большой для представления целых чисел порядка J ** макс(|С|)* @param C — матрица размерностей JxW, такая что C[j][w] = стоимость назначения j-го* работа для работника (возможно отрицательно)** @return вектор длины J, где j-й элемент равен минимальной стоимости* назначить первые (j+1) рабочих мест отдельным работникам*/шаблон < класс T > вектор < T > венгерский ( const вектор < вектор < T >> & C ) { const int J = ( int ) размер ( C ), W = ( int ) размер ( C [ 0 ]); утверждение ( J <= W ); // job[w] = задание, назначенное w-му работнику, или -1, если задание не назначено // примечание: W-й работник был добавлен для удобства вектор < int > задание ( W + 1 , -1 ); вектор < T > ys ( J ), yt ( W + 1 ); // потенциалы // -yt[W] будет равен сумме всех дельт вектор < T > ответы ; const T inf = numeric_limits < T >:: max (); for ( int j_cur = 0 ; j_cur < J ; ++ j_cur ) { // назначаем j_cur-ое задание int w_cur = W ; задание [ w_cur ] = j_cur ; // минимальное снижение стоимости по ребрам от Z до рабочего w вектор < T > min_to ( W + 1 , inf ); vector < int > prv ( W + 1 , -1 ); // предыдущий рабочий на альтернативном пути vector < bool > in_Z ( W + 1 ); // находится ли работник в Z while ( job [ w_cur ] != -1 ) { // выполняется максимум j_cur + 1 раз in_Z [ w_cur ] = true ; const int j = job [ w_cur ]; Т дельта = инф ; int w_next ; для ( int w = 0 ; w < W ; ++ w ) { если ( ! in_Z [ w ]) { если ( ckmin ( min_to [ w ], C [ j ][ w ] - ys [ j ] - yt [ w ])) prv [ w ] = w_cur ; если ( ckmin ( delta , min_to [ w ])) w_next = w ; } } // дельта всегда будет неотрицательной, // за исключением, возможно, первого раза, когда этот цикл выполняется // если какие-либо записи C[j_cur] отрицательны для ( int w = 0 ; w <= W ; ++ w ) { если ( in_Z [ w ]) ys [ job [ w ]] += delta , yt [ w ] -= delta ; иначе min_to [ w ] -= delta ; } w_cur = w_next ; } // обновить назначения по альтернативному пути для ( int w ; w_cur != W ; w_cur = w ) задание [ w_cur ] = задание [ w = prv [ w_cur ]]; ответы . push_back ( - yt [ W ]); } верните ответы ; }/*** Проверка работоспособности: https://en.wikipedia.org/wiki/Hungarian_algorithm/Hungarian_algorithm#Example* Первая работа (5):* чистая ванная комната: Боб -> 5* Первая + вторая работа (9):* чистая ванная комната: Боб -> 5* подметать полы: Алиса -> 4* Первая + вторая + третья работа (15):* чистая ванная комната: Элис -> 8* подметать полы: Кэрол -> 4* мытье окон: Боб -> 3*/void sanity_check_hungarian () { вектор < вектор < int >> стоит {{ 8 , 5 , 9 }, { 4 , 2 , 4 }, { 7 , 3 , 8 }}; assert (( венгерский ( затраты ) == вектор < int > { 5 , 9 , 15 })); cerr << "Проверка на работоспособность пройдена. \n " ; }// решает https://open.kattis.com/problems/cordonbleuvoid cordon_bleu () { целые N , M ; cin >> N >> M ; вектор < пара < целое , целое >> B ( N ), C ( M ); вектор < пара < целое , целое >> бутылки ( N ), курьеры ( M ); для ( auto & b : bottles ) cin >> b . first >> b . second ; для ( auto & c : couriers ) cin >> c . first >> c . second ; пара < целое , целое > остаток ; cin >> rest . первый >> rest . второй ; вектор < вектор < int >> затраты ( N , вектор < int > ( N + M - 1 )); auto dist = [ & ]( пара < целое , целое > x , пара < целое , целое > y ) { вернуть abs ( x.first - y.first ) + abs ( x.second - y.second ) ; }; для ( int b = 0 ; b < N ; ++ b ) { for ( int c = 0 ; c < M ; ++ c ) { // курьер -> бутылка -> ресторан затраты [ б ][ с ] = dist ( курьеры [ c ], бутылки [ b ]) + dist ( бутылки [ b ], остальное ); } for ( int _ = 0 ; _ < N - 1 ; ++ _ ) { // ресторан -> бутылка -> ресторан затраты [ b ][ _ + M ] = 2 * расст ( бутылки [ b ], остальное ); } } cout << венгерский ( стоимость ). назад () << " \n " ; }инт мейн () { проверка_здравоохранения_венгерский (); cordon_bleu ();}
Венгерский алгоритм можно рассматривать как эквивалент последовательного алгоритма кратчайшего пути для потока минимальной стоимости, [8] [9] , где метод перевеса из алгоритма Джонсона используется для поиска кратчайших путей. Реализация из предыдущего раздела переписывается ниже таким образом, чтобы подчеркнуть эту связь; можно проверить, что потенциалы для рабочих равны потенциалам из предыдущего решения с точностью до постоянного смещения. Когда граф разрежен (есть только разрешенные пары работа, рабочий), можно оптимизировать этот алгоритм для выполнения во времени, используя кучу Фибоначчи для определения вместо итерации по всем рабочим, чтобы найти тот, который имеет минимальное расстояние (упоминается здесь ).
шаблон < класс T > вектор < T > венгерский ( const вектор < вектор < T >> & C ) { const int J = ( int ) размер ( C ), W = ( int ) размер ( C [ 0 ]); утверждение ( J <= W ); // job[w] = задание, назначенное w-му работнику, или -1, если задание не назначено // примечание: W-й работник был добавлен для удобства вектор < int > задание ( W + 1 , -1 ); вектор < T > h ( W ); // потенциалы Джонсона вектор < T > ответы ; T ans_cur = 0 ; const T inf = numeric_limits < T >:: max (); // назначить j_cur-th задание, используя Дейкстру с потенциалами для ( int j_cur = 0 ; j_cur < J ; ++ j_cur ) { int w_cur = W ; // не посещенный работник с минимальным расстоянием задание [ w_cur ] = j_cur ; вектор < T > dist ( W + 1 , inf ); // Джонсон-редуцированные расстояния расст [ Вт ] = 0 ; вектор < bool > vis ( W + 1 ); // посещались ли уже vector < int > prv ( W + 1 , -1 ); // предыдущий рабочий на кратчайшем пути while ( job [ w_cur ] != -1 ) { // Шаг Дейкстры: извлечь минимальный рабочий процесс из кучи T min_dist = инф ; vis [ w_cur ] = true ; int w_next = -1 ; // следующий не посещенный работник с минимальным расстоянием // рассмотрим расширение кратчайшего пути с помощью w_cur -> job[w_cur] -> w для ( int w = 0 ; w < W ; ++ w ) { если ( ! вис [ w ]) { // сумма приведенных весов ребер w_cur -> job[w_cur] -> w T edge = C [ job [ w_cur ]][ w ] - h [ w ]; если ( w_cur != W ) { edge -= C [ job [ w_cur ]][ w_cur ] - h [ w_cur ]; assert ( edge >= 0 ); // следствие потенциалов Джонсона } если ( ckmin ( dist [ w ], dist [ w_cur ] + edge )) prv [ w ] = w_cur ; если ( ckmin ( min_dist , dist [ w ])) w_next = w ; } } w_cur = w_next ; } for ( int w = 0 ; w < W ; ++ w ) { // обновить потенциалы ckmin ( dist [ w ], dist [ w_cur ]); ч [ ш ] += расст [ ш ]; } ans_cur += h [ w_cur ]; для ( int w ; w_cur != W ; w_cur = w ) задание [ w_cur ] = задание [ w = prv [ w_cur ]]; ответы . push_back ( ans_cur ); } верните ответы ; }
Этот вариант алгоритма следует формулировке, данной Флудом [10], и позднее более подробно описанной Манкресом, который доказал, что он работает во времени. [4] Вместо того, чтобы отслеживать потенциалы вершин, алгоритм работает только с матрицей:
где — исходная матрица стоимости, а — потенциалы из интерпретации графика. Изменение потенциалов соответствует добавлению или вычитанию из строк или столбцов этой матрицы. Алгоритм начинается с . Таким образом, его можно рассматривать как взятие исходной матрицы стоимости и ее изменение.
При наличии n работников и задач задача записывается в виде матрицы затрат n × n .
где a, b, c и d — работники, которые должны выполнить задачи 1, 2, 3 и 4. a 1 , a 2 , a 3 и a 4 обозначают штрафы, возникающие, когда работник «a» выполняет задачи 1, 2, 3 и 4 соответственно.
Проблема эквивалентна назначению каждому работнику уникальной задачи таким образом, чтобы общий штраф был минимизирован. Обратите внимание, что над каждой задачей может работать только один работник.
Для каждой строки ее минимальный элемент вычитается из каждого элемента в этой строке. Это приводит к тому, что все элементы имеют неотрицательные значения. Таким образом, назначение с общим штрафом 0 по определению является минимальным назначением.
Это также приводит к по крайней мере одному нулю в каждой строке. Таким образом, наивный жадный алгоритм может попытаться назначить всем работникам задачу со штрафом в ноль. Это проиллюстрировано ниже.
Нули выше — это назначенные задачи.
В худшем случае есть n ! комбинаций, которые нужно попробовать, поскольку несколько нулей могут появиться в строке, если несколько элементов являются минимальными. Поэтому в какой-то момент этот наивный алгоритм должен быть сокращен.
Иногда может оказаться, что матрица на данном этапе не может быть использована для назначения, как в случае с матрицей ниже.
Чтобы преодолеть это, мы повторяем описанную выше процедуру для всех столбцов (т.е. минимальный элемент в каждом столбце вычитается из всех элементов в этом столбце ), а затем проверяем, возможно ли назначение со штрафом 0.
В большинстве случаев это даст результат, но если это все еще невозможно, то нужно продолжать.
Все нули в матрице должны быть покрыты путем маркировки как можно меньшего количества строк и/или столбцов. Шаги 3 и 4 образуют один из способов достижения этого.
Для каждой строки попробуйте назначить произвольный ноль. Назначенные задачи представлены звездочкой с нулем. Обратите внимание, что назначения не могут находиться в одной строке или столбце.
Мы могли бы закончить другим заданием, если бы выбрали другой порядок строк и столбцов.
Охватите все столбцы, содержащие (отмеченный звездочкой) ноль.
Найдите не покрытый ноль и сделайте его штрихом (отметьте его символом штриха ). Если такой ноль не найден, то есть все нули покрыты, перейдите к шагу 5.
Ноль в строке 3 не закрыт. Добавляем к пути первый ноль строки 1, затем второй ноль строки 1, и все готово.
Все нули теперь покрыты минимальным количеством строк и столбцов.
Вышеупомянутое подробное описание — это всего лишь один из способов нарисовать минимальное количество линий, чтобы покрыть все 0. Другие методы тоже работают.
Если количество отмеченных нулей равно n (или в общем случае , где n — количество людей, а m — количество рабочих мест), алгоритм завершается. См. подраздел Результат ниже о том, как интерпретировать результаты.
В противном случае найдите наименьшее непокрытое значение. Вычтите это из каждого непомеченного элемента и прибавьте к каждому элементу, покрытому двумя линиями. Вернитесь к шагу 4.
Это эквивалентно вычитанию числа из всех строк, которые не покрыты, и добавлению того же числа ко всем столбцам, которые покрыты. Эти операции не изменяют оптимальные назначения.
Если следовать этой конкретной версии алгоритма, то отмеченные звездочкой нули образуют минимальное назначение.
Из теоремы Кёнига [11] минимальное количество линий (минимальное вершинное покрытие [12] ) будет равно n (размер максимального паросочетания [13] ). Таким образом, когда требуется n линий, назначение минимальной стоимости можно найти, рассматривая только нули в матрице.
Обратите внимание, что не все из них удовлетворяют временной сложности, даже если они так заявляют. Некоторые могут содержать ошибки, реализовывать более медленный алгоритм или иметь другие недостатки. В худшем случае пример кода, связанный с Википедией, может быть позже изменен для включения кода эксплойта. Проверка и бенчмаркинг необходимы при использовании таких примеров кода от неизвестных авторов.