1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Двудольность: когда вершины делятся на две команды

Коротко

Граф двудольный, если вершины можно разложить на две группы так, что каждое ребро соединяет вершины из РАЗНЫХ групп - внутри группы рёбер нет. Это то же самое, что раскрасить граф в два цвета правильно (соседи разного цвета). Проверяется обычным обходом: красим стартовую вершину в первый цвет, каждого соседа - в противоположный; если наткнулись на ребро, соединяющее вершины ОДНОГО цвета, граф не двудолен. Цена O(V + E). И есть точный критерий, объясняющий, что именно мешает: граф двудолен тогда и только тогда, когда в нём нет циклов нечётной длины. Треугольник не двудолен, квадрат двудолен, дерево двудольно всегда.

Если ты знаешь определение двудольности, умеешь проверять её обходом за O(V + E) и помнишь критерий «нет нечётных циклов», листай до «Теперь сам».

Как думаешь, что мешает разделить на две команды

Есть компания людей, некоторые друг с другом в ссоре. Надо рассадить их за два стола так, чтобы поссорившиеся оказались за разными. Прежде чем читать дальше, придумай самую маленькую компанию, в которой это НЕВОЗМОЖНО. Сколько в ней человек и как устроены ссоры?

На что это похоже

Шахматная доска. Покрась клетки в два цвета по правилу «соседние - разного цвета»; получится привычная шахматная раскраска, и она существует, потому что любой замкнутый маршрут по клеткам имеет чётную длину. А теперь представь доску, где три клетки попарно соседние, - такую раскрасить двумя цветами нельзя, как ни старайся: третья клетка обязательно совпадёт по цвету с одной из первых двух.

Механизм: красим обходом

color = [-1] * V                 # -1 не покрашена, 0 и 1 - две доли

def bipartite():
    for s in range(V):
        if color[s] != -1:
            continue
        color[s] = 0
        q = deque([s])
        while q:
            v = q.popleft()
            for u in adj[v]:
                if color[u] == -1:
                    color[u] = 1 - color[v]      # противоположный цвет
                    q.append(u)
                elif color[u] == color[v]:
                    return False                 # ребро внутри доли
    return True

Три замечания к коду. Первое: обход годится любой - в ширину или в глубину, разницы нет, красим мы при первом посещении. Второе: внешний цикл обязателен - граф может быть несвязным, и каждую компоненту красят независимо (её стартовую вершину можно класть в любую долю). Третье: конфликт ищется на РЁБРАХ, а не на вершинах: пока не наткнулись на ребро между одноцветными, всё в порядке.

12 43 цикл длины 4 - двудолен abc цикл длины 3 - двудольным не бывает
Чётный цикл чередует цвета и замыкается правильно; нечётный при обходе по кругу приводит к ребру между вершинами одного цвета.

Правило

Граф двудолен, если вершины делятся на две группы без рёбер внутри группы; это равносильно правильной раскраске в два цвета. Проверяется обходом за O(V + E): красим стартовую вершину, соседей - в противоположный цвет, и ищем ребро между одноцветными. Каждую компоненту красят независимо. Точный критерий: граф двудолен ⇔ в нём НЕТ циклов нечётной длины. Деревья и леса двудольны всегда, треугольник - никогда.

Разбор: почему мешают именно нечётные циклы

Докажем в обе стороны - это короткое и красивое рассуждение.

Если граф двудолен, нечётных циклов нет. Пусть есть цикл v₁ → v₂ → ... → v_k → v₁. Цвета по рёбрам чередуются: v₁ в доле 0, v₂ в доле 1, v₃ снова в 0 и так далее. Значит цвет вершины v_i определяется чётностью i. Чтобы цикл замкнулся, v_k должна быть противоположного цвета относительно v₁, то есть k должно быть чётным. Нечётный цикл замкнуться не может.

Если нечётных циклов нет, граф двудолен. Раскрасим так: возьмём в каждой компоненте стартовую вершину s и покрасим вершину v в цвет расстояние(s, v) mod 2. Покажем, что это правильная раскраска. Пусть нашлось ребро u-v между вершинами одного цвета, то есть расстояния до них имеют одинаковую чётность. Тогда путь от s до u, ребро u-v и путь от v обратно к s образуют замкнутый маршрут длины d(u) + 1 + d(v) - а это число НЕЧЁТНО (сумма двух чисел одной чётности плюс единица). Замкнутый маршрут нечётной длины обязательно содержит нечётный цикл (общие куски путей проходятся дважды и дают чётный вклад, который можно выбросить). Противоречие с условием.

Отсюда и практический бонус алгоритма: когда обход находит конфликтное ребро, он не просто говорит «не двудолен» - по дереву обхода легко восстановить сам нечётный цикл и показать его человеку как доказательство. В задачах про конфликты это ровно то объяснение, которое нужно: «вот три условия, которые невозможно удовлетворить одновременно».

Разбор: проверяем граф руками

Граф: 1-2, 2-3, 3-4, 4-1, 1-3. Начинаем с вершины 1, обход в ширину.

  1. color[1] = 0. Очередь: [1].
  2. Достаём 1. Соседи: 2, 4, 3. Все не покрашены → color[2] = color[4] = color[3] = 1. Очередь: [2, 4, 3].
  3. Достаём 2. Соседи: 1 (цвет 0, отличается - хорошо), 3 (цвет 1, а у самой двойки цвет 1) - конфликт, граф не двудолен.

Где именно спрятался нечётный цикл? В треугольнике 1-2-3-1: рёбра 1-2, 2-3 и 1-3 есть, длина цикла три. Заметь, что квадрат 1-2-3-4-1 сам по себе был бы двудольным - проблему создало «диагональное» ребро 1-3.

Что ломается без этого

Первое: забыть внешний цикл и проверить только компоненту стартовой вершины - несвязный граф с треугольником во второй компоненте будет объявлен двудольным. Второе: сравнивать цвета вершин, а не концов ребра: двудольность нарушает именно ребро, и находить его надо при просмотре списков смежности. Третье: считать, что двудольность связана с чётностью числа вершин или рёбер - не связана вовсе: путь из трёх вершин двудолен, треугольник из трёх - нет. Четвёртое: применять проверку к ориентированному графу, забыв, что направления здесь не играют роли: для двудольности граф рассматривают как неориентированный. Пятое: думать, что раскраска единственна - в связной компоненте она единственна с точностью до обмена цветов, а в несвязном графе каждую компоненту можно переворачивать независимо, так что вариантов 2^(число компонент).

Зачем это в работе

Двудольность - первый вопрос в целом классе задач «разделить на два» и «сопоставить одно другому». Распределение задач по двум ресурсам, разделение процессов на два узла так, чтобы конфликтующие не встретились, проверка схемы на возможность развести в два слоя. Но главное применение - вход в теорию ПАРОСОЧЕТАНИЙ: задачи «назначить работников на работы», «студентов на проекты», «водителей на маршруты» формулируются как поиск максимального паросочетания в двудольном графе, и там есть быстрые алгоритмы - но сперва граф должен быть двудольным, а это как раз проверяется обходом. Ещё пример из практики: проверка непротиворечивости системы ограничений вида «эти двое должны быть по разные стороны» - конфликт находится вместе с нечётным циклом, который можно показать как объяснение. И заметь общий мотив главы: двудольность - это раскраска в ДВА цвета, и она решается за линейное время, а уже для трёх цветов задача становится вычислительно трудной. Разница между двумя и тремя здесь принципиальная - об этом следующий урок.

Теперь сам

Граф: 1-2, 2-3, 3-4, 4-5, 5-1. Двудолен ли он? А если добавить ребро 1-3?

Ответ. Это цикл длины пять: 1-2-3-4-5-1. Попробуем покрасить: color[1]=0, color[2]=1, color[3]=0, color[4]=1, color[5]=0. Теперь ребро 5-1: у пятёрки цвет 0 и у единицы цвет 0 - конфликт, граф НЕ двудолен. Это согласуется с критерием: цикл нечётной длины (пять) двудольным быть не может, потому что цвета чередуются и при возврате в начало не сходятся. Теперь добавим ребро 1-3. Граф от этого двудольнее не станет - нечётный цикл 1-2-3-4-5-1 никуда не делся, а вдобавок появился ещё один нечётный цикл 1-2-3-1 длины три. Ответ: не двудолен в обоих случаях. Полезная проверка понимания: а если из исходного пятиугольника УБРАТЬ любое ребро? Тогда останется путь из пяти вершин - дерево, а деревья двудольны всегда, потому что циклов в них нет вовсе. Вот и весь критерий в одном примере: дело не в числе вершин и не в числе рёбер, а исключительно в чётности циклов.

Главное

Граф двудолен, если вершины делятся на две группы без рёбер внутри группы - это то же самое, что правильная раскраска в два цвета. Проверка - обычный обход за O(V + E): красим стартовую вершину в цвет 0, каждого нового соседа в противоположный, и ищем ребро между одноцветными; внешний цикл по вершинам обязателен, потому что каждую компоненту красят независимо. Точный критерий: граф двудолен ⇔ в нём нет циклов НЕЧЁТНОЙ длины. Доказательство в обе стороны короткое: цвета вдоль цикла чередуются, поэтому замкнуться цикл может только при чётной длине; обратно - раскраска по чётности расстояния от старта корректна, иначе нашёлся бы замкнутый маршрут нечётной длины, а в нём нечётный цикл. Практический бонус: вместе с ответом «не двудолен» алгоритм умеет предъявить сам нечётный цикл - готовое объяснение, почему разделить нельзя. Деревья двудольны всегда, треугольник - никогда, и дело не в числе вершин, а только в чётности циклов. Двудольность - вход в задачи о паросочетаниях, а также граница простоты: две краски проверяются за линию, три - уже трудная задача.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.