# теория · шаг 3 из 7
Паросочетание в общем графе: цветки Эдмондса и веса
Коротко
В двудольном графе дополняющий путь ищут обходом: каждая вершина в дереве поиска либо «чётная», либо
«нечётная», и это раз и навсегда. В графе с НЕЧЁТНЫМ циклом вершину можно достичь и чётным путём, и
нечётным, и обход, пометивший её не той стороной, пропускает существующий путь. Алгоритм Эдмондса
находит такой цикл - цветок - и сжимает его в одну вершину: дополняющий путь в сжатом графе
разворачивается в путь в исходном, обходя цветок с нужной стороны. Получается наибольшее паросочетание
в любом графе за O(V³). Для весов есть его взвешенная версия той же сложности; простая жадность
(тяжёлые рёбра первыми) гарантирует лишь половину оптимума.
Если ты знаешь, почему нечётный цикл ломает поиск дополняющего пути, как сжимают цветок и что гарантирует жадность на весах, листай до «Теперь сам».
Как думаешь, что мешает алгоритму Куна на любом графе
Пары для командной работы: любой может работать с любым, если они знакомы, - граф не двудольный. Нужно составить как можно больше пар. Алгоритм Куна из прошлого урока ищет дополняющий путь обходом и двудольность нигде не проверяет.
Прежде чем читать дальше, попробуй угадать: на графе с циклом из пяти вершин код Куна запустится и что-то вернёт. Будет ли ответ наибольшим? Если нет - где именно поиск свернёт не туда?
На что это похоже
Лабиринт с кольцевым коридором. Ты отмечаешь мелом развилки: «пришёл сюда, шагая с левой ноги». Правило лабиринта: выйти можно только с правой. В лабиринте без колец нога в каждой развилке определена раз и навсегда. Кольцо нечётной длины позволяет прийти в ту же развилку и с левой ноги, и с правой - если обойти кольцо с другой стороны. Пометка мелом «здесь был, с левой» закрывает выход, который на самом деле есть. Эдмондс делает простую вещь: кольцо целиком считается одной развилкой, в которую можно прийти «с любой ноги».
Механизм: внешние вершины, цветок, сжатие
поиск дополняющего пути от свободной вершины r:
r - внешняя (чётная); сосед u внешней вершины:
u свободна → путь найден, перевернуть его
u в паре с x → u внутренняя (нечётная),
x внешняя, продолжаем от x
u уже ВНЕШНЯЯ → ребро между двумя внешними:
нечётный цикл - ЦВЕТОК
сжать цветок в его основание (ближайшую к r вершину цикла):
все вершины цветка становятся внешними
продолжать поиск в сжатом графе
найденный путь, проходящий через цветок, разворачивается:
внутри цветка идут по той стороне цикла, где чередование
«свободное - в паре» сходится
Почему сжатие ничего не теряет: в цветке из 2k + 1 вершин k рёбер в паре, и основание - единственная
вершина, которая «смотрит наружу» парой. До любой вершины цикла от основания можно дойти и чётным, и
нечётным путём - значит, через любую из них путь может выйти наружу. Сжатая вершина выражает ровно
это: «из цветка можно выйти где угодно».
Веса. Паросочетание наибольшего веса в двудольном графе решал венгерский алгоритм, в общем - его
взвешенная версия Эдмондса, O(V³), с теми же цветками и потенциалами на вершинах и на нечётных
множествах. Реализация длинная, и в работе её берут из библиотеки. Важно знать гарантию запасного
варианта: жадность (сортировка рёбер по убыванию веса) даёт не меньше половины оптимума - каждое её
ребро «закрывает» не больше двух рёбер оптимума, и оба не тяжелее его.
Правило
В графе с нечётными циклами дополняющий путь обходом «как в двудольном» можно пропустить: вершина
цикла достижима и чётным, и нечётным путём. Алгоритм Эдмондса, встретив ребро между двумя внешними
вершинами, сжимает найденный нечётный цикл (цветок) в основание и продолжает поиск; путь в сжатом
графе разворачивается в исходный. Наибольшее паросочетание - O(V³), наибольший вес - взвешенная
версия той же сложности; жадность по весам гарантирует только половину.
Разбор: цикл из пяти и две свободные вершины
рёбра: 0–2 0–3 1–3 1–4 1–5 2–4 2–5 пары сейчас: 0–2, 1–3
свободны: 4 и 5. Цикл 4–1–3–0–2–4 из пяти вершин
поиск от 4 «как в двудольном»:
4 внешняя → соседи 1 и 2 внутренние, их пары 3 и 0 внешние
от 3: соседи 0, 1 уже помечены; от 0: соседи 2, 3 помечены
5 соседствует только с 1 и 2 - а они ВНУТРЕННИЕ, из них не выходят
→ пути нет, ответ 2
Эдмондс: та же разметка, но ребро 0–3 соединяет две ВНЕШНИЕ вершины
→ цикл 4–1–3–0–2–4 - цветок, сжимаем его в основание 4,
все его вершины, включая 1 и 2, становятся внешними
из 2 ребро к свободной 5 → путь найден
разворот через цветок чётной стороной: 5–2, 2–0, 0–3, 3–1, 1–4
переворот: пары 5–2, 0–3, 1–4 - три пары
В обходе «как в двудольном» вершина 2 получила метку «внутренняя», потому что её первой достигли
нечётным путём 4–2. Тот же 2 достижим и чётным путём 4–1–3–0–2 - и именно из него открывается
выход к 5. Цветок объединяет обе возможности.
Разбор: триста случайных графов
Программа реализует алгоритм Эдмондса и обход без сжатия цветков, сравнивает их с точным перебором динамикой по маскам на трёхстах графах из 14 вершин, а на взвешенных - жадность с оптимумом.
import random
from collections import deque
from functools import lru_cache
def edmonds(n, sos): # наибольшее паросочетание, цветки
para = [-1] * n
def dopolnit(koren):
otec, baza = [-1] * n, list(range(n))
v_dereve, och = [False] * n, deque([koren])
v_dereve[koren] = True
def obshchij_predok(a, b): # LCA двух вершин в дереве поиска
byl = [False] * n
while True:
a = baza[a]
byl[a] = True
if para[a] == -1:
break
a = otec[para[a]]
while True:
b = baza[b]
if byl[b]:
return b
b = otec[para[b]]
def pometit(v, b, rebenok, cvetok):
while baza[v] != b: # идём по цветку к его основанию
cvetok[baza[v]] = cvetok[baza[para[v]]] = True
otec[v] = rebenok
rebenok = para[v]
v = otec[para[v]]
while och:
v = och.popleft()
for u in sos[v]:
if baza[v] == baza[u] or para[v] == u:
continue
vneshnyaya = para[u] != -1 and otec[para[u]] != -1
if u == koren or vneshnyaya:
b = obshchij_predok(v, u) # нечётный цикл - цветок
cvetok = [False] * n
pometit(v, b, u, cvetok)
pometit(u, b, v, cvetok)
for x in range(n): # сжать цветок в основание
if cvetok[baza[x]]:
baza[x] = b
if not v_dereve[x]:
v_dereve[x] = True
och.append(x)
elif otec[u] == -1:
otec[u] = v
if para[u] == -1: # свободная - путь найден
while u != -1:
pv = para[otec[u]]
para[u], para[otec[u]] = otec[u], u
u = pv
return True
v_dereve[para[u]] = True
och.append(para[u])
return False
return sum(dopolnit(v) for v in range(n) if para[v] == -1)
def bez_cvetkov(n, sos): # «как в двудольном»: обход без
para, razmer = [-1] * n, 0 # сжатия нечётных циклов
for koren in range(n):
if para[koren] != -1:
continue
otec, och = {koren: None}, deque([koren])
while och:
v = och.popleft()
for u in sos[v]:
if u in otec or para[v] == u:
continue
otec[u] = v
if para[u] == -1:
while u is not None:
pv = para[otec[u]]
para[u], para[otec[u]] = otec[u], u
u = pv if pv != -1 else None
razmer += 1
och.clear()
break
otec[para[u]] = u
och.append(para[u])
return razmer
def perebor(n, ves): # точный ответ динамикой по маскам
@lru_cache(None)
def f(maska):
v = next((v for v in range(n) if not maska >> v & 1), None)
if v is None:
return 0
luchshe = f(maska | 1 << v) # v остаётся без пары
for u in range(n):
if not maska >> u & 1 and (v, u) in ves:
luchshe = max(luchshe,
ves[v, u] + f(maska | 1 << v | 1 << u))
return luchshe
return f(0)
def zhadno(ves): # самое тяжёлое ребро первым
zanyato, itog = set(), 0
for (u, v), w in sorted(ves.items(), key=lambda x: -x[1]):
if u not in zanyato and v not in zanyato:
zanyato |= {u, v}
itog += w
return itog
def graf(n, rebra):
sos = [[] for _ in range(n)]
for u, v in rebra:
sos[u].append(v)
sos[v].append(u)
return sos
rebra = [(0, 2), (0, 3), (1, 3), (1, 4), (1, 5), (2, 4), (2, 5)]
print("граф с циклом 0-2-4-1-3: Эдмондс", edmonds(6, graf(6, rebra)),
" без цветков", bez_cvetkov(6, graf(6, rebra)))
rng = random.Random(21)
oshibok, hudshee = 0, 1.0
for opyt in range(300):
n = 14
rebra = [(u, v) for u in range(n) for v in range(u + 1, n)
if rng.random() < 0.2]
edin, ves = {}, {}
for u, v in rebra:
edin[u, v] = edin[v, u] = 1
ves[u, v] = ves[v, u] = rng.randint(1, 100)
tochno = perebor(n, edin)
assert edmonds(n, graf(n, rebra)) == tochno
oshibok += bez_cvetkov(n, graf(n, rebra)) < tochno
if ves:
hudshee = min(hudshee, zhadno(ves) / perebor(n, ves))
print("300 графов: Эдмондс совпал с перебором всегда,",
"без цветков ошибся", oshibok, "раз")
print(f"веса: жадность в худшем случае {hudshee:.2f} от оптимума")
граф с циклом 0-2-4-1-3: Эдмондс 3 без цветков 2
300 графов: Эдмондс совпал с перебором всегда, без цветков ошибся 4 раз
веса: жадность в худшем случае 0.70 от оптимума
Эдмондс совпал с перебором на всех трёхстах графах; обход без цветков ошибся четыре раза - редко, и
поэтому опасно: тесты на нескольких графах ошибку легко пропустят. Жадность по весам в худшем из
трёхсот случаев набрала 70% оптимума - выше гарантированной половины, но заметно ниже ста.
Что ломается без этого
Первая ошибка - запускать Куна на недвудольном графе: код работает, ответ правдоподобный и иногда меньше наибольшего, без единого сообщения об ошибке. Вторая - при сжатии цветка не пометить внешними ВСЕ его вершины: путь, выходящий из внутренней вершины цикла, останется ненайденным. Третья - развернуть путь через цветок не той стороной цикла: чередование «свободное - в паре» нарушится, и после переворота у вершины окажется две пары. Четвёртая - считать жадное паросочетание по весам оптимальным: гарантия - лишь половина.
Зачем это в работе
Пары без двух «сторон»: напарники в командах, соседи по комнатам, обмен донорскими органами между парами «донор - реципиент», объединение мелких грузов в пары по совместимости, раскладка задач на двухъядерные машины. Паросочетание в общем графе - строительный блок других алгоритмов: оно стоит в задаче китайского почтальона (нечётные вершины разбивают на пары паросочетанием минимального веса) и в алгоритме Кристофидеса для коммивояжёра.
Теперь сам
Треугольник a–b–c и ребро c–d. Пара сейчас: a–b. Есть ли дополняющий путь? Как поведёт себя
поиск от d?
Ответ. Свободны c и d, и ребро c–d - уже дополняющий путь длины один: паросочетание
{a–b, c–d} из двух пар. Поиск от d: сосед c свободен - путь найден сразу. Цветок a–b–c здесь не
понадобился бы, но если бы пара была b–c, а свободны a и d, путь d–c–b–a нашёлся бы обходом
d → c → b → a, и цикл снова не помешал: нечётный цикл опасен, только когда нужный выход лежит на
«не той» стороне.
Главное
Нечётный цикл позволяет прийти в вершину и чётным, и нечётным путём, поэтому обход «как в двудольном»
теряет дополняющие пути - в программе 4 ошибки на 300 графов, без единого предупреждения. Алгоритм
Эдмондса сжимает такой цикл-цветок в основание и находит наибольшее паросочетание в любом графе за
O(V³); взвешенная версия той же сложности решает задачу с весами, а жадность гарантирует лишь
половину оптимума.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий