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

# теория · шаг 4 из 8

Включение-исключение: гамильтонов цикл без таблицы на 2^n

Коротко

Принцип включения-исключения считает объекты, у которых нет НИ ОДНОГО «плохого» свойства: всего, минус обладающие каждым свойством, плюс обладающие каждой парой, минус каждой тройкой - и так далее. Сколько чисел до ста делятся на 2, 3 или 5? 50 + 33 + 20 − 16 − 10 − 6 + 3 = 74. Тот же принцип считает гамильтоновы циклы: замкнутый обход длины n из вершины 0 - гамильтонов, если не пропустил ни одной вершины; обходы, которые ИЗБЕГАЮТ заданного множества, считаются простой динамикой по шагам. Сумма со знаками по всем 2^(n−1) множествам даёт ответ за O(2^n · n³) - как у динамики по маскам по порядку, но с памятью O(n) вместо таблицы на 2^n · n чисел.

Если ты умеешь считать включением-исключением и знаешь, как через него посчитать гамильтоновы циклы в полиномиальной памяти, листай до «Теперь сам».

Как думаешь, что мешает динамике по маскам на 30 вершинах

Динамика по маскам из прошлого урока решает коммивояжёра на n = 20. Её таблица - 2^n · n чисел: на двадцати вершинах это двадцать миллионов, на тридцати - тридцать миллиардов, больше любой памяти. Время 2^30 действий - около миллиарда - ещё терпимо, а память уже нет.

Прежде чем читать дальше, попробуй угадать: можно ли считать по всем подмножествам, НЕ храня ничего про каждое из них? Подсказка: иногда легче посчитать «всё подряд», а потом вычесть лишнее, чем сразу считать только правильное.

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

Перепись гостей, которые попробовали ВСЕ блюда на банкете. Спрашивать каждого о каждом блюде долго. Зато легко узнать, сколько тарелок ушло без каждого блюда: все гости, минус те, кто пропустил салат, минус те, кто пропустил суп, но тех, кто пропустил и то и другое, мы вычли дважды - прибавим обратно. Каждый раз задаётся простой вопрос про «исключённые блюда», а ответ про «все блюда» собирается знакопеременной суммой.

Механизм: знаки по подмножествам и обходы в обход

ВКЛЮЧЕНИЕ-ИСКЛЮЧЕНИЕ
свойства P_1 … P_m (плохие); N(S) - сколько объектов обладает
всеми свойствами из множества S
объектов без единого плохого свойства =  Σ по S  (−1)^|S| · N(S)

ГАМИЛЬТОНОВЫ ЦИКЛЫ
объекты:  замкнутые обходы длины n из вершины 0 (с повторами вершин)
плохое свойство P_v:  обход ни разу не зашёл в v
обход длины n без плохих свойств посетил все n вершин,
    ровно по разу - это гамильтонов цикл

N(S) = обходов длины n из 0 в 0, не заходящих в S:
    динамика по шагам: schet[v] - сколько путей длины t в v
    по разрешённым вершинам; n шагов по O(n²) - O(n³)

циклов = Σ по S ⊆ {1 … n−1}  (−1)^|S| · N(S)
2^(n−1) множеств, на каждое O(n³) времени и O(n) памяти

Почему каждый «неправильный» обход сократится. Пусть обход пропустил ровно множество вершин M (не пустое). Он учтён во всех N(S) с S ⊆ M - со знаком (−1)^|S|. Подмножеств M с чётным размером столько же, сколько с нечётным, и сумма знаков равна нулю. Остаются только обходы с M = ∅ - гамильтоновы.

Правило

Включение-исключение считает объекты без плохих свойств как Σ по S (−1)^|S| · N(S), где N(S) - число объектов со всеми свойствами из S. Гамильтоновы циклы через вершину 0 - это замкнутые обходы длины n, не пропустившие ни одной вершины; N(S) - обходы, избегающие S, считаются динамикой по шагам за O(n³). Всего O(2^n · n³) времени и O(n) памяти против таблицы 2^n · n у динамики по маскам.

Разбор: делители и треугольник

ЧИСЛА 1..100, делящиеся на 2, 3 или 5
    на 2: 50    на 3: 33    на 5: 20
    на 6: 16    на 10: 10   на 15: 6
    на 30: 3
    50 + 33 + 20 − 16 − 10 − 6 + 3 = 74

ГАМИЛЬТОНОВЫ ЦИКЛЫ В ТРЕУГОЛЬНИКЕ 0–1–2, длина обхода 3
    S = ∅:      замкнутые обходы длины 3 из 0: 0-1-2-0 и 0-2-1-0
                (0-1-0-1 за три шага в 0 не вернулся) → 2
    S = {1}:    только 0 и 2: 0-2-0-2 не возвращается за 3 шага → 0
    S = {2}:    0
    S = {1, 2}: из 0 никуда → 0
    циклов = 2 − 0 − 0 + 0 = 2  (один цикл в двух направлениях)

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

Разбор: двенадцать вершин двумя способами

Программа считает гамильтоновы циклы случайного графа на 12 вершинах динамикой по маскам (Хелд-Карп) и включением-исключением и сравнивает ответы и память.

vklyuchenie.py
import itertools
import random

rng = random.Random(27)
n = 12
sosedi = [[] for _ in range(n)]
for u in range(n):
    for v in range(u + 1, n):
        if rng.random() < 0.45:
            sosedi[u].append(v)
            sosedi[v].append(u)


def held_karp():                      # маски: путей из 0 через набор
    tabl = [[0] * n for _ in range(2 ** n)]   # 2^n · n чисел в памяти
    tabl[1][0] = 1
    for maska in range(1, 2 ** n):
        for v in range(n):
            if tabl[maska][v]:
                for u in sosedi[v]:
                    if not maska // 2 ** u % 2:
                        tabl[maska + 2 ** u][u] += tabl[maska][v]
    polnaya = 2 ** n - 1
    return sum(tabl[polnaya][v] for v in sosedi[0]), 2 ** n * n


def vklyuchenie_isklyuchenie():       # память - один массив на n чисел
    itog = 0
    drugie = list(range(1, n))
    for r in range(n):
        for ubrany in itertools.combinations(drugie, r):
            mozhno = [True] * n
            for x in ubrany:
                mozhno[x] = False
            schet = [0] * n           # замкнутые обходы длины n из 0
            schet[0] = 1              # только по разрешённым вершинам
            for _ in range(n):
                novyj = [0] * n
                for v in range(n):
                    if schet[v]:
                        for u in sosedi[v]:
                            if mozhno[u]:
                                novyj[u] += schet[v]
                schet = novyj
            itog += (-1) ** r * schet[0]
    return itog, n


a, pam_a = held_karp()
b, pam_b = vklyuchenie_isklyuchenie()
print("гамильтоновых циклов (с направлением):", a, b)
print("память: таблица", pam_a, "чисел против", pam_b)
↳  выводpython3 vklyuchenie.py
гамильтоновых циклов (с направлением): 34 34
память: таблица 49152 чисел против 12

Ответы совпали: 34 направленных цикла, то есть 17 различных. Таблица динамики - 49 152 числа, у включения-исключения в памяти одновременно - массив из двенадцати. На тридцати вершинах таблица потребовала бы тридцать миллиардов чисел, а включение-исключение - те же тридцать, хотя считать придётся долго: 2^29 множеств.

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

Первая ошибка - забыть знак: без (−1)^|S| сумма считает обходы с повторениями многократно. Вторая - считать обходами пути, а не замкнутые обходы, или взять длину не n: гамильтонов цикл из n вершин - ровно n рёбер. Третья - ждать, что приём находит сам цикл: он СЧИТАЕТ (и отвечает «есть ли» по ненулевому ответу), а чтобы восстановить цикл, нужна ещё работа - например, удалять рёбра и смотреть, не упал ли ответ до нуля. Четвёртая - переполнение в языках с фиксированной шириной чисел: промежуточные суммы бывают огромными, а результат маленький; Python спасает длинная арифметика.

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

Включение-исключение - главный инструмент комбинаторного счёта: сколько паролей содержит хотя бы одну цифру и хотя бы одну букву, сколько перестановок без неподвижных точек, сколько чисел меньше n не имеют с ним общих делителей. В алгоритмах точного перебора оно даёт решения, которым хватает полиномиальной памяти: раскраска графа в k цветов, покрытие множествами, задачи о расписаниях. Когда время 2^n терпимо, а память 2^n - нет, стоит спросить, нельзя ли считать «в обход» со знаками.

Теперь сам

Сколько чисел от 1 до 60 не делятся ни на 2, ни на 3, ни на 5?

Ответ. Делятся хотя бы на одно: 30 + 20 + 12 − 10 − 6 − 4 + 2 = 44. Не делятся ни на одно: 60 − 44 = 16. Проверка: это числа без общих делителей с 30, на отрезке длиной в два периода 30, - по 8 на период (1, 7, 11, 13, 17, 19, 23, 29), всего 16.

Главное

Включение-исключение считает «ни одного плохого свойства» знакопеременной суммой по множествам свойств. Гамильтонов цикл - замкнутый обход длины n, не пропустивший ни одной вершины, а обходы в обход заданного множества считает простая динамика. Отсюда O(2^n · n³) времени при памяти O(n): на двенадцати вершинах тот же ответ, что у динамики по маскам, без таблицы на 49 тысяч чисел.

Комментарии

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

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

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