Алгоритм Хопкрофта — Карпа


Алгоритм Хопкрофта — Карпа — алгоритм, принимающий на вход двудольный граф и возвращающий максимальное по мощности паросочетание, то есть наибольшее множество рёбер, таких что никакие два не имеют общую вершину. Асимптотика времени работы алгоритма составляет O ( | E | | V | ) {displaystyle O(|E|{sqrt {|V|}})} в худшем случае (здесь E {displaystyle E} — множество рёбер графа, а V {displaystyle V} — множество его вершин). В случае плотных графов время работы ограничивается O ( | V | 2.5 ) {displaystyle O(|V|^{2.5})} , а для случайного графа алгоритм работает почти за линейное время.

Алгоритм был создан Джоном Хопкрофтом и Ричардом Карпом в 1973 году. Как и ранее созданные алгоритмы (такие как венгерский алгоритм и алгоритм из работы Эдмондса, алгоритм Хопкрофта — Карпа в цикле увеличивает паросочетание, находя увеличивающие пути (цепи, рёбра которых попеременно принадлежат паросочетанию и не принадлежат ему, причём первая и последняя вершина не принадлежат паросочетанию; выполнив чередование паросочетания вдоль цепи, то есть убрав из паросочетания рёбра, бывшие в цепи, и добавив те, которых в нём не было, можно получить большее паросочетание). Вместо того чтобы находить один увеличивающий путь, алгоритм находит максимальное множество кратчайших увеличивающих путей. В результате требуется лишь O ( | V | ) {displaystyle O({sqrt {|V|}})} итераций. Та же идея используется в более сложных алгоритмах для поиска паросочетаний в недвудольных графах с тем же асимптотическим временем работы, как у алгоритма Хопкрофта — Карпа.

Описание задачи

Задачу поиска наибольшего паросочетания в двудольном графе неформально можно описать следующим образом. Имеется группа мальчиков и девочек. Некоторые мальчики знакомы с некоторыми девочками. Требуется составить как можно больше пар для танца, состоящих из мальчика и девочки, знакомых друг с другом.

Увеличивающие пути

Вершина, которая не является концом какого-либо ребра паросочетания M {displaystyle M} , называется свободной вершиной (для этого паросочетания). Алгоритм основан на понятии увеличивающего пути — пути, который начинается и заканчивается в свободной вершине, а внутри пути ребра, принадлежащие и не принадлежащие паросочетанию M {displaystyle M} , чередуются. Из определения следует, что все вершины такого пути, кроме первой и последней, обязаны быть несвободными. Увеличивающий путь может состоять из двух свободных вершин и одного ребра между ними (которое не лежит в паросочетании).

Если M {displaystyle M} — паросочетание и P {displaystyle P} — увеличивающий путь в M {displaystyle M} , то симметрическая разность двух множеств рёбер M ⊕ P {displaystyle Moplus P} является паросочетанием размера | M | + 1 {displaystyle |M|+1} . Таким образом, найдя увеличивающие пути, можно увеличить размер паросочетания.

Наоборот, пусть M {displaystyle M} не оптимально и пусть P {displaystyle P} есть симметрическая разность M ⊕ M ∗ {displaystyle Moplus M^{*}} , где M ∗ {displaystyle M^{*}} — оптимальное паросочетание. Поскольку M {displaystyle M} и M ∗ {displaystyle M^{*}} — паросочетания, каждая вершина в P {displaystyle P} имеет степень не более двух. Значит, P {displaystyle P} должно образовывать множество непересекающихся увеличивающих путей и циклов или путей, в которых рёбер из паросочетания столько же, сколько и не из него. Разность размеров M {displaystyle M} и M ∗ {displaystyle M^{*}} и есть число увеличивающих путей в P {displaystyle P} . Итак, если существует паросочетание M ∗ {displaystyle M^{*}} , большее текущего паросочетания M {displaystyle M} , также должен существовать увеличивающий путь. Если увеличивающего пути не существует, можно успешно прервать алгоритм, так как M {displaystyle M} должно быть оптимальным.

Увеличивающие пути в задачах о паросочетаниях тесно связаны с увеличивающими путями, возникающими в задачах о максимальном потоке, путями, вдоль которых можно увеличить поток между истоком и стоком. Можно свести задачу о поиске наибольшего паросочетания к задаче поиска максимального потока. Технику, используемую в алгоритме Хопкрофта — Карпа, можно обобщить для произвольной транспортной сети, что приведёт к алгоритму Диница.

Алгоритм

Ниже представлена структура алгоритма:

Вход: Двудольный граф G ( U ∪ V , E ) {displaystyle G(Ucup V,E)} Выход: Паросочетание M ⊆ E {displaystyle Msubseteq E} M ← ∅ {displaystyle Mleftarrow emptyset } цикл P ← { P 1 , P 2 , … , P k } {displaystyle {mathcal {P}}leftarrow {P_{1},P_{2},dots ,P_{k}}} максимальное множество непересекающихся по вершинам кратчайших увеличивающих путей M ← M ⊕ ( P 1 ∪ P 2 ∪ ⋯ ∪ P k ) {displaystyle Mleftarrow Moplus (P_{1}cup P_{2}cup dots cup P_{k})} пока P ≠ ∅ {displaystyle {mathcal {P}} eq emptyset }

Более подробно, пусть даны U {displaystyle U} и V {displaystyle V} — множества вершин двудольного графа G {displaystyle G} , а E {displaystyle E} — множество его рёбер, соединяющих вершины из U {displaystyle U} и V {displaystyle V} . Алгоритм, начиная с пустого паросочетания M {displaystyle M} , последовательно его увеличивает. На каждой фазе алгоритм делает следующее:

  • Поиск в ширину (BFS) разбивает вершины графа на слои. BFS начинает с множества свободных вершин U {displaystyle U} , которые тем самым образуют первый слой разбиения (таким образом, первый слой содержит только не покрытые паросочетанием рёбра). На последующих уровнях поиска алгоритм добавляет вершины на новый уровень, чередуя рёбра: будет попеременно добавлять вершины, связанные ребром то в паросочетании, то вне его, поэтому в процессе поиска из вершины из U {displaystyle U} мы всегда будем проходить по рёбрам не из паросочетания, а если вершина из V {displaystyle V} — то наоборот. Поиск прерывается на уровне k {displaystyle k} , как только впервые будет достигнута хотя бы одна свободная вершина из V {displaystyle V} .
  • Все свободные вершины из V {displaystyle V} на этом слое k {displaystyle k} обозначим как F {displaystyle F} . Получается, вершина v {displaystyle v} принадлежит F {displaystyle F} тогда и только тогда, когда в ней кончается кратчайший удлиняющий путь.
  • Алгоритм находит максимальное множество непересекающихся по вершинам путей длины k {displaystyle k} . Это множество может быть найдено поиском в глубину (DFS), который использует найденное ранее разбиение на слои. Поиск может проходить лишь по рёбрам, которые ведут в неиспользованные вершины предыдущего слоя, и путь в дереве поиска в глубину должен чередоваться относительно паросочетания M {displaystyle M} . Как только увеличивающий путь войдёт в одну из вершин F {displaystyle F} , следует начать DFS от следующей вершины.
  • Каждый из путей, который будет найден, используется для увеличения M {displaystyle M} .

Алгоритм прерывается, когда на какой-либо фазе BFS не находит ни одного увеличивающего пути (то есть F {displaystyle F} пусто).

Анализ

Каждая фаза состоит из одного BFS и одного DFS, поэтому одна фаза выполняется за O ( | E | ) {displaystyle O(|E|)} . Следовательно, первые | V | {displaystyle {sqrt {|V|}}} фаз в графе с | V | {displaystyle |V|} вершинами и | E | {displaystyle |E|} рёбрами имеют стоимость O ( | E | | V | ) {displaystyle O(|E|{sqrt {|V|}})} . Можно показать, что каждая фаза увеличивает длину кратчайшего удлиняющего пути, по крайней мере, на 1: фаза находит максимальный набор дополняющих путей данной длины, так что любой оставшийся путь должен быть длиннее. Следовательно, после того как первые | V | {displaystyle {sqrt {|V|}}} фаз алгоритма завершились, кратчайший оставшийся увеличивающий путь имеет длину, по крайней мере, | V | {displaystyle {sqrt {|V|}}} . Однако симметрическая разность оптимального паросочетания и текущего паросочетания M {displaystyle M} , найденного в предыдущих фазах, образует множество вершинно непересекающихся увеличивающих путей и чередующихся циклов. Если каждый путь имеет длину, по крайней мере, | V | {displaystyle {sqrt {|V|}}} , может быть не более чем | V | {displaystyle {sqrt {|V|}}} путей, и размер оптимального паросочетания может отличаться от размера M {displaystyle M} не более чем на | V | {displaystyle {sqrt {|V|}}} . Так как каждая фаза алгоритма увеличивает размер паросочетания, по крайней мере, на 1, ещё может произойти не более | V | {displaystyle {sqrt {|V|}}} фаз.

Так как в худшем случае алгоритм имеет 2 | V | {displaystyle 2{sqrt {|V|}}} фаз, общее время работы составляет O ( | E | | V | ) {displaystyle O(|E|{sqrt {|V|}})} в худшем случае.

Однако во многих случаях алгоритм может быть гораздо быстрее, чем говорит оценка в худшем случае. Например, в случае разреженного двудольного случайного графа в 2006 году было показано (улучшая предыдущий результат), что с большой вероятностью все неоптимальные паросочетания имеют увеличивающие пути логарифмической длины. Как следствие, для таких графов количество итераций O ( log ⁡ | V | ) {displaystyle O(log |V|)} , а время работы алгоритма составляет O ( | E | log ⁡ | V | ) {displaystyle O(|E|log |V|)} .

Сравнение с другими алгоритмами поиска максимального паросочетания

Для разреженных графов алгоритм Хопкрофта — Карпа имеет наилучшую асимптотику в худшем случае из всех известных алгоритмов, но для плотных графов более новый алгоритм имеет слегка лучшую границу O ( n 1.5 m log ⁡ n ) {displaystyle Oleft(n^{1.5}{sqrt {frac {m}{log n}}} ight)} . Этот алгоритм базируется на алгоритме проталкивания предпотока, а, когда паросочетание становится близко к оптимальному, переключается на метод Хопкрофта — Карпа.

Несколько авторов провели экспериментальное сравнение алгоритмов поиска максимального паросочетания. Их результаты показали, что в общем случае алгоритм Хопкрофта — Карпа на практике не так хорош, как в теории: его превосходят по производительности как простые BFS и DFS-стратегии по поиску увеличивающего пути, так и алгоритмы, основанные на методе проталкивания предпотока.

Недвудольные графы

Та же идея поиска максимального множества кратчайших увеличивающих путей также работает для нахождения паросочетаний максимальной мощности в недвудольных графах, и по тем же причинам алгоритм будет иметь не более O ( | V | ) {displaystyle O({sqrt {|V|}})} фаз. Однако для недвудольных графов поиск удлиняющих путей в каждой фазе более сложен. Базируясь на более ранних работах, Micali & Vazirani (1980) показали, как выполнять фазу за линейное время, что привело к созданию алгоритма с той же верхней оценкой, что и у алгоритма Хопкрофта — Карпа для двудольных графов. Метод Micali — Vazirani сложен, и авторы не предоставили полных доказательств своих результатов; впоследствии Peterson & Loui (1988) опубликовали полное обоснование алгоритма Micali — Vazirani, а также были опубликованы другие алгоритмы: Gabow & Tarjan (1991) и Blum (2001). В 2012 году Vazirani предложил новое, упрощённое доказательство алгоритма Micali — Vazirani.

Псевдокод

Ниже приведён псевдокод алгоритма, близкий к реализации на Java.

/* G = U ∪ V ∪ {NIL} where U and V are partition of graph and NIL is a special null vertex */ function BFS () for u in U if Pair_U[u] == NIL Dist[u] = 0 Enqueue(Q,u) else Dist[u] = ∞ Dist[NIL] = ∞ while Empty(Q) == false u = Dequeue(Q) if Dist[u] < Dist[NIL] for each v in Adj[u] if Dist[ Pair_V[v] ] == ∞ Dist[ Pair_V[v] ] = Dist[u] + 1 Enqueue(Q,Pair_V[v]) return Dist[NIL] != ∞ function DFS (u) if u != NIL for each v in Adj[u] if Dist[ Pair_V[v] ] == Dist[u] + 1 if DFS(Pair_V[v]) == true Pair_V[v] = u Pair_U[u] = v return true Dist[u] = ∞ return false return true function Hopcroft-Karp for each u in U Pair_U[u] = NIL for each v in V Pair_V[v] = NIL matching = 0 while BFS() == true for each u in U if Pair_U[u] == NIL if DFS(u) == true matching = matching + 1 return matching

Пояснения

Пусть граф состоит из долей U и V. Ключевая идея состоит в добавлении двух фиктивных вершин с каждой стороны графа: uDummy, соединённую со всеми непокрытыми вершинами из U, и vDummy, соединённую со всеми непокрытыми вершинами из V. Теперь, если мы запустим BFS из uDummy в vDummy, мы получим кратчайший путь между непокрытой вершиной из U и непокрытой вершиной из V. Из-за двудольности графа путь будет чередоваться между U и V. Однако нужно удостовериться, что, идя из V в U, мы всегда выбираем ребро из паросочетания. Если не осталось вершин из паросочетания, то мы заканчиваем в vDummy. Руководствуясь этим критерием в процессе BFS, в конце получим кратчайший увеличивающий путь.

После того, как кратчайший увеличивающий путь будет найден, надо игнорировать все пути, которые длиннее его. BFS помечает вершины, расстояние от которых до истока 0. После выполнения BFS мы можем, стартуя из каждой вершины из U, не лежащей в паросочетании, идя по пути, в котором расстояние до следующей вершины больше расстояния до предыдущей на 1. Если мы в конце приходим в vDummy, расстояние до которой на 1 больше, чем расстояние до одной из вершин из V, по которой можно дойти по одному из кратчайших путей. В таком случае мы можем пойти дальше и обновить паросочетание для вершин на пути. Заметим, что каждая вершина V на пути, кроме самой последней, уже в паросочетании. Поэтому обновление паросочетания эквивалентно симметрической разности (то есть удалению тех рёбер пути, которые были в паросочетании, и добавлению тех, которых не было.

Как удостовериться, что увеличивающие пути не пересекаются по вершинам? Это уже обеспечено. После выполнения симметрической разности никакая из вершин пути не будет рассмотрена ещё раз, потому что Dist[ Pair_V[v] ] не будет равна Dist[u] + 1 (а будет в точности Dist[u]).

Зачем нужны следующие строчки?

Dist[u] = ∞ return false

Когда мы не можем найти никакой кратчайший увеличивающий путь из вершины u, DFS возвращает False. В этом случае, будет хорошо пометить эти вершины, чтобы не посещать их ещё раз. Чтобы отметить их, мы просто выставляем Dist[u] бесконечным.

Нам не нужен uDummy, потому что он лишь для того, чтобы складывать все вершины не из паросочетания в очередь BFS. Это можно сделать просто инициализацией. vDummy можно добавить в U для удобства во многих реализациях, и инициализировать паросочетание для всех вершин из V указателем на vDummy. Поэтому если в конце концов последняя вершина из U не находится в паросочетании ни с одной вершиной из V, то последней вершиной нашего удлиняющего пути будет vDummy. В псевдокоде выше vDummy обозначается Nil.