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

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

Формула Кэли и коды Прюфера: n^(n−2) деревьев и как выбрать одно наугад

Коротко

Сколько разных деревьев можно построить на n пронумерованных вершинах? Формула Кэли: ровно n^(n−2) - для пяти вершин 125, для десяти - сто миллионов. Доказывает её код Прюфера - взаимно однозначное соответствие между деревьями и последовательностями длины n − 2 из чисел 0..n−1. Кодирование: n − 2 раза отрезать наименьший лист и записать его соседа. Декодирование: степень вершины - на единицу больше числа её появлений в коде, и рёбра восстанавливаются тем же правилом «наименьший лист». Отсюда и практичный приём: случайное дерево, равномерное среди всех n^(n−2), - это декодированная случайная последовательность.

Если ты умеешь строить код Прюфера и восстанавливать дерево по коду и понимаешь, почему отсюда следует n^(n−2), листай до «Теперь сам».

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

Пять пронумерованных серверов надо соединить кабелями без циклов так, чтобы все были связаны, - это дерево. Сколько существует разных схем? Перебрать четыре ребра из десяти возможных - 210 наборов, и часть из них содержит цикл.

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

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

Разборка мебели с подписью деталей. Каждый раз снимаешь самую маленькую по номеру деталь, которая держится только за одну другую, и записываешь, к чему она крепилась. Запись получается короткой - по числу на каждую снятую деталь, кроме двух последних, - и по ней однозначно собирается та же мебель: сборщик знает, сколько раз каждая деталь встречается в записи, а значит, сколько к ней крепится.

Механизм: отрезать наименьший лист

КОДИРОВАНИЕ (дерево → код длины n − 2)
повторить n − 2 раза:
    лист = наименьший по номеру лист
    записать его соседа, удалить лист

ДЕКОДИРОВАНИЕ (код → дерево)
степень(v) = 1 + сколько раз v встречается в коде
для каждого x в коде по порядку:
    лист = наименьшая вершина со степенью 1
    ребро лист – x;  степень(x) −= 1 (лист больше не участвует)
последние две вершины со степенью 1 соединить ребром

Почему соответствие взаимно однозначное. Лист никогда не попадает в код, пока он лист: вершина встречается в коде ровно (степень − 1) раз - столько её соседей удалят до неё самой. Значит, по коду восстанавливаются степени, а по степеням - какая вершина на каждом шаге была наименьшим листом, и, следовательно, всё дерево. Каждая последовательность длины n − 2 декодируется в дерево, разные коды - в разные деревья. Последовательностей n^(n−2) - столько и деревьев.

С кучей для наименьшего листа оба направления работают за O(n log n).

Правило

Деревьев на n пронумерованных вершинах ровно n^(n−2) (формула Кэли). Код Прюфера сопоставляет дереву последовательность из n − 2 чисел: отрезать наименьший лист и записать его соседа. Обратно: степень = 1 + число появлений в коде, и рёбра восстанавливаются правилом «наименьший лист - к очередному числу кода». Случайный код даёт равномерно случайное дерево.

Разбор: туда и обратно на шести вершинах

рёбра: 0–3, 1–3, 2–3, 3–4, 4–5

КОДИРОВАНИЕ
листья {0, 1, 2, 5}:  наименьший 0, сосед 3 → код 3
листья {1, 2, 5}:     1 → 3
листья {2, 5}:        2 → 3;  теперь 3 - лист (остался сосед 4)
листья {3, 5}:        3 → 4
код: 3 3 3 4

ДЕКОДИРОВАНИЕ
степени: 3 встречается трижды → 4;  4 один раз → 2;  остальные 1
x = 3: наименьший лист 0 → ребро 0–3, степень 3 стала 3
x = 3: лист 1 → 1–3, степень 3 → 2
x = 3: лист 2 → 2–3, степень 3 → 1 - теперь 3 лист
x = 4: наименьший лист 3 → 3–4, степень 4 → 1
остались листья 4 и 5 → ребро 4–5

Дерево восстановилось полностью. Вершина 3 со степенью четыре встретилась в коде трижды, листья 0, 1, 2, 5 - ни разу: это всегда так, и по коду сразу видно, какие вершины - листья.

Разбор: проверка формулы перебором

Программа перебирает все наборы из n − 1 ребра полного графа, считает среди них деревья для n от 2 до 7 и сравнивает с n^(n−2), затем проверяет, что все 1296 кодов длины 4 дают 1296 РАЗНЫХ деревьев на шести вершинах.

pryufer.py
import heapq
import itertools
import random


def v_kod(n, rebra):                  # дерево → код Прюфера
    sosedi = {v: set() for v in range(n)}
    for u, v in rebra:
        sosedi[u].add(v)
        sosedi[v].add(u)
    listya = [v for v in range(n) if len(sosedi[v]) == 1]
    heapq.heapify(listya)
    kod = []
    for _ in range(n - 2):
        list_ = heapq.heappop(listya)  # наименьший лист
        otec = sosedi[list_].pop()
        kod.append(otec)               # записать его соседа
        sosedi[otec].discard(list_)
        if len(sosedi[otec]) == 1:
            heapq.heappush(listya, otec)
    return kod


def iz_koda(n, kod):                  # код Прюфера → дерево
    stepen = [1] * n
    for x in kod:
        stepen[x] += 1                # степень = 1 + появлений в коде
    listya = [v for v in range(n) if stepen[v] == 1]
    heapq.heapify(listya)
    rebra = []
    for x in kod:
        list_ = heapq.heappop(listya)
        rebra.append((list_, x))
        stepen[x] -= 1
        if stepen[x] == 1:
            heapq.heappush(listya, x)
    rebra.append(tuple(listya))        # два последних листа
    return rebra


def derevo_li(n, rebra):               # n − 1 ребро и связность
    kto = list(range(n))
    def koren(x):
        while kto[x] != x:
            x = kto[x]
        return x
    for u, v in rebra:
        a, b = koren(u), koren(v)
        if a == b:
            return False
        kto[a] = b
    return True


for n in range(2, 8):                  # перебор: остовы полного графа
    vse_rebra = list(itertools.combinations(range(n), 2))
    derevev = sum(derevo_li(n, r)
                  for r in itertools.combinations(vse_rebra, n - 1))
    print(f"n={n}: деревьев {derevev}, n^(n−2) = {n ** (n - 2)}")

n = 6                                  # код ↔ дерево - биекция
vidennye = set()
for kod in itertools.product(range(n), repeat=n - 2):
    d = iz_koda(n, list(kod))
    assert derevo_li(n, d) and v_kod(n, d) == list(kod)
    vidennye.add(frozenset(frozenset(e) for e in d))
print(f"n=6: кодов {n ** (n - 2)}, разных деревьев {len(vidennye)}")

rng = random.Random(29)
kod = [rng.randrange(10) for _ in range(8)]
print("дерево на 10 вершинах из кода", kod)
print(sorted(tuple(sorted(e)) for e in iz_koda(10, kod)))
↳  выводpython3 pryufer.py
n=2: деревьев 1, n^(n−2) = 1
n=3: деревьев 3, n^(n−2) = 3
n=4: деревьев 16, n^(n−2) = 16
n=5: деревьев 125, n^(n−2) = 125
n=6: деревьев 1296, n^(n−2) = 1296
n=7: деревьев 16807, n^(n−2) = 16807
n=6: кодов 1296, разных деревьев 1296
дерево на 10 вершинах из кода [8, 1, 5, 9, 9, 4, 1, 8]
[(0, 8), (1, 2), (1, 4), (1, 8), (3, 5), (4, 7), (5, 9), (6, 9), (8, 9)]

Перебор подтвердил формулу до семи вершин (на семи - 16 807 деревьев из 54 264 наборов рёбер), а 1296 кодов дали 1296 разных деревьев - соответствие взаимно однозначно. Последняя строка - дерево, полученное из случайного кода: так генерируют тестовые деревья, равномерные по всем возможным.

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

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

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

Генерация тестов для алгоритмов на деревьях: случайный код Прюфера даёт по-настоящему равномерное дерево, в том числе длинные цепочки и звёзды. Компактное хранение и передача деревьев: n − 2 числа вместо n − 1 пары. Подсчёт вариантов в задачах проектирования сетей: сколько всего способов связать n узлов без избыточных связей. А для числа остовов произвольного графа есть матричная теорема о деревьях Кирхгофа - определитель, обобщающий формулу Кэли.

Теперь сам

Восстанови дерево на пяти вершинах 0..4 по коду 1 1 3. Какие вершины - листья?

Ответ. Степени: 1 встречается дважды → 3, 3 один раз → 2, остальные 1. Листья {0, 2, 4}. x = 1: лист 0 → ребро 0–1, степень 1 → 2. x = 1: лист 2 → 2–1, степень 1 → 1, теперь 1 лист. x = 3: наименьший лист 1 → 1–3, степень 3 → 1. Остались 3 и 4 → 3–4. Рёбра: 0–1, 1–2, 1–3, 3–4; листья - 0, 2, 4 - ровно те, кого нет в коде.

Главное

Деревьев на n пронумерованных вершинах n^(n−2), и код Прюфера это доказывает: отрезать наименьший лист и записать соседа - получается последовательность из n − 2 чисел, а по ней, зная степени как «1 + число появлений», дерево восстанавливается однозначно. Перебор подтвердил формулу до семи вершин; случайный код - самый простой способ получить равномерно случайное дерево.

Комментарии

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

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

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