Система непересекающихся множеств (DSU)

Города, дороги и компоненты связности. Представители, объединение по размеру, сжатие путей и первые шаги алгоритма Краскала.

01

Множества и две операции

Представьте шесть городов 0…5. Город — вершина графа, дорога между двумя городами — ребро. Из 0 в 2 можно попасть по пути 0→1→2, даже без прямой дороги 0—2. Компонента связности — все города, между которыми можно проехать; изолированный город тоже образует компоненту. После появления каждой новой дороги нужно быстро проверять, какие города связаны.

DSU / СНМ (Disjoint-set union) — Структура, поддерживающая разбиение элементов на попарно непересекающиеся множества и операции find и union.

В прошлой лекции мы соединяли списки; теперь нужна другая информация: принадлежат ли элементы одной группе. После дорог 0—1 и 1—2 города 0, 1, 2 образуют одну компоненту. Добавленная 0—2 уже не уменьшит число групп. Проследите эту последовательность в таблице, затем создайте дороги самостоятельно.

Добавленная дорогаКомпоненты на картеЧисло компонент
Пока дорог нет{0}, {1}, {2}, {3}, {4}, {5}6
0—1, затем 1—2{0,1,2}, {3}, {4}, {5}4
3—4{0,1,2}, {3,4}, {5}3
0—2{0,1,2}, {3,4}, {5}: связь уже была3

Вначале каждый элемент — отдельное множество. find(x) возвращает его представителя. union(x,y) объединяет классы элементов x и y, не меняя остальные.

Можно использовать DSU для добавляемых рёбер графа: чтобы проверить, соединены ли u и v, достаточно сравнить их представителей.

Компоненты при добавлении рёбер
012345

Кружки — города (вершины), линии — дороги (рёбра). Путь может проходить через несколько дорог. Ссылки parent ниже — служебный лес DSU: они не обязаны быть дорогами.

0 → 0[0]
1 → 1[1]
2 → 2[2]
3 → 3[3]
4 → 4[4]
5 → 5[5]

Представители: 0, 1, 2, 3, 4, 5. Компонент: 6.

Каждая вершина пока является своей компонентой.

ОперацияСмысл
makeSet(x)Создать множество {x}
find(x)Получить представитель множества x
union(x,y)Объединить классы x и y

Если union(0,1) и union(1,2) уже выполнены, то 0, 1 и 2 должны иметь одного представителя, даже если ребра (0,2) нет. Это транзитивная связь, а не проверка непосредственного соседства. Повторный union внутри одной компоненты ничего не меняет и не должен уменьшать счётчик компонент.

Размер компоненты для каждой вершины

Сначала объединяем концы всех рёбер, затем читаем size только у представителя. Изолированная вершина имеет размер 1. Общая цена O(n + (n+m)α(n)), память O(n).

Размер компоненты для каждой вершины / Python
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
        self.components = n
    def find(self, x):
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        while self.parent[x] != x:
            following = self.parent[x]
            self.parent[x] = root
            x = following
        return root
    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        self.components -= 1
        return True
def component_sizes(n, edges):
    d = DSU(n)
    for a, b in edges:
        d.union(a, b)
    return [d.size[d.find(v)] for v in range(n)]

print(*component_sizes(6, [(0, 1), (1, 2), (3, 4)]))
Ожидаемый вывод
3 3 3 2 2 1

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Группы распространения новости

В Codeforces 1167C все участники одной группы соединяются с её первым участником. Для группы из k человек достаточно k−1 объединений, а не всех k² пар.

Контракт определён. Теперь выберем представление, которое позволит объединять компоненты без переписывания всех их элементов.

Города и дороги: предскажите компоненты

Задания раздела

У 6 городов добавлены дороги 0—1, 1—2, 3—4, затем 0—2. Сколько компонент осталось?

Какие пары связаны после этих дорог?

Попыток: 0
02

Лес деревьев представителей

Этот раздел откроется после предыдущих проверок.

Продолжить: Множества и две операции →

Разбиение на непересекающиеся множества удобно хранить лесом. У каждой вершины ровно один parent, а корень ссылается сам на себя. Сам номер представителя не важен — важно равенство представителей.

Каждый набор элементов хранится деревом указателей parent. В корне parent[root] = root; он и является представителем. Запрос find поднимается по ссылкам до корня.

Базовая реализация
makeSet(x):
    parent[x] = x
    rank[x] = 0

find(x):
    if parent[x] == x: return x
    return find(parent[x])

Без эвристик деревья могут стать высокими, и find может стоить Θ(n).

Лес представителей
0 → 0[0]
1 → 1[1]
2 → 2[2]
3 → 3[3]
4 → 4[4]
5 → 5[5]

Представители: 0, 1, 2, 3, 4, 5. Компонент: 6.

Каждая вершина пока является своей компонентой.

Объединять только корниСклейка произвольных внутренних узлов может нарушить деревообразную структуру. Сначала выполняются find(x), find(y).

Вызов union сначала вычисляет find для обоих аргументов, затем меняет parent одного корня. Если переподвесить произвольную внутреннюю вершину, можно оторвать часть её старой компоненты или создать цикл. Массив size относится к корням; размер компоненты x читают как size[find(x)].

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Представитель не обязан быть минимумом

Если ответ должен содержать минимальный id компоненты, храните его отдельным полем у корня и обновляйте при union. Правило выбора корня по размеру само по себе не сохраняет минимальный номер.

Лес хранит ответ корректно, но его форма пока может быть плохой. Ограничим высоту правилом объединения.

Проследите parent и выберите объединяемые корни

Задания раздела

parent=[0,0,1,3,3,5]. Каков find(2)?

Для union(2,4) какой parent допустимо изменить?

Попыток: 0
03

Эвристика рангов

Этот раздел откроется после предыдущих проверок.

Продолжить: Множества и две операции →

Наивное объединение позволяет каждый раз подвешивать длинное дерево под новый корень. После n−1 таких операций получится цепочка. Сначала освоим объединение по размеру: корень компоненты из 3 вершин подвесим под корень компоненты из 7, а в новом корне запишем размер 10. Номер корня при этом не важен.

Наивная цепочка и объединение по размеру

Добавляем новые вершины 1…7 к компоненте 0. Наивный вариант подвешивает старый корень под новую вершину; вариант по размеру сохраняет больший корень.

Выполнено объединений: 0. Глубина 0: наивно 0, по размеру 0.

0 → 0[0]
1 → 1[1]
2 → 2[2]
3 → 3[3]
4 → 4[4]
5 → 5[5]
6 → 6[6]
7 → 7[7]
0 → 0[0]
1 → 1[1]
2 → 2[2]
3 → 3[3]
4 → 4[4]
5 → 5[5]
6 → 6[6]
7 → 7[7]

Компоненты имеют размеры 3 (корень 2) и 7 (корень 5). Как выполнить union по размеру?

Union по размеру
union(a,b):
    a, b = find(a), find(b)
    if a == b: return false
    if size[a] < size[b]: swap(a,b)
    parent[b] = a
    size[a] += size[b]
    return true

Размер читается только у корня; повторный union ничего не меняет.

Путь вершины становится длиннее только при присоединении её меньшей компоненты к большей. Тогда размер как минимум удваивается: 1, 2, 4, 8… Поэтому без сжатия высота не превосходит O(log n). Ранг — другой способ выбирать направление; его анализ можно прочитать после практики с размером.

Ранг (Rank) — Вспомогательное значение корня, ограничивающее высоту дерева при union по рангу.

При объединении корень меньшего ранга подвешивается под корень большего. Если ранги равны, один корень подвешивается к другому и ранг нового корня увеличивается на единицу.

Углубление: логарифмическая граница ранга
Почему ранг ограничен логарифмом
ранг 0: ≥ 1[0]
ранг 1: ≥ 2[1]
ранг 2: ≥ 4[2]
ранг 3: ≥ 8[3]

Для ранга 3 нужно не меньше 8 вершин. Два корня ранга 2 дают следующий ранг только при равенстве рангов.

После сжатия ранг остаётся исторической верхней оценкой, а не измеренной текущей высотой.

Углубление: rank(root) ≤ ⌊log₂ |set|⌋
Чтобы корень получил ранг k, его дерево должно содержать как минимум 2ᵏ вершин. Ранг увеличивается только при объединении двух равных рангов: два дерева по 2ᵏ узлов дают не меньше 2ᵏ⁺¹.

Следовательно, без сжатия путей один find занимает O(log n). Равноправный вариант — union по размеру: меньший набор под больший.

Ранг и высота после сжатия
Сжатие путей уменьшает реальные глубины, но ранги не пересчитывают. Поэтому после добавления второй эвристики ранг — сохранённая оценочная величина, а не обязательно точная текущая высота.

Для обязательной реализации используйте уже разобранный union по размеру. Ранг — равноправная альтернатива, а строгое исследование оценок одной и двух эвристик вынесено в дополнительные главы. Совместное применение размера и сжатия даёт очень малую амортизированную цену длинной последовательности операций.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Плохой порядок объединений

Вход может сначала объединить 0 с 1, затем всю компоненту с 2 и так далее. Произвольное направление образует цепочку; эвристика размера не зависит от удачного порядка запросов.

Ранги дали логарифмический путь. Используем сам факт прохождения пути, чтобы ускорить следующие запросы.

Примените объединение по размеру

Задания раздела

Корни 2 и 5 имеют размеры 3 и 7. Что записать?

До union(2,5) было 4 компоненты. Сразу повторили union(2,5). Сколько компонент теперь?

Попыток: 0
04

Эвристика сжатия путей

Этот раздел откроется после предыдущих проверок.

Продолжить: Множества и две операции →

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

Сжатие путей (Path compression) — После find(x) все вершины пройденного пути начинают непосредственно указывать на найденный корень.

Первый запрос может пройти несколько ссылок, но он ускоряет будущие запросы. Сжатие путей не меняет разбиение на множества: меняется только форма дерева.

Сжатие конкретного пути
0 → 0[0]
1 → 0[1]
2 → 1[2]
3 → 2[3]
4 → 3[4]
5 → 4[5]

Представители: 0, 0, 0, 0, 0, 0. Компонент: 1.

Начальное дерево специально вытянуто: сравните ссылки до и после find(5).

find со сжатием путей
find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])
    return parent[x]

Важно изменить parent[x], а не просто вычислить корень.

Типичный нюансОценка одного запроса в худшем случае по-прежнему может превышать константу. Почти константной становится амортизированная стоимость последовательности операций.

Сжатие меняет только ссылки внутри прежней компоненты. После него ранг уже не обязан совпадать с текущей высотой: уменьшать ранги после find не нужно. Итеративный вариант сначала находит корень, затем проходит путь второй раз; он не расходует стек вызовов на длинной наивной цепочке.

Объединение по размеру и сжатие путей

Find выполнен двумя проходами без рекурсии. Union возвращает false для уже связанных вершин, поэтому счётчик компонент не уменьшается повторно. Инициализация O(n), m операций — O(m α(n)) амортизированно, память O(n). Индексы должны лежать в [0,n).

Объединение по размеру и сжатие путей / Python
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
        self.components = n
    def find(self, x):
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        while self.parent[x] != x:
            following = self.parent[x]
            self.parent[x] = root
            x = following
        return root
    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        self.components -= 1
        return True
d = DSU(6)
for a, b in [(0, 1), (1, 2), (3, 4), (0, 2)]:
    d.union(a, b)
print(d.components, d.size[d.find(2)])
Ожидаемый вывод
3 3

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Много запросов связности

Сначала добавлены дороги, затем часто спрашивается связь тех же городов. Сжатие запоминает представителей для посещённых вершин, поэтому повторные find обходят меньше ссылок.

Реализация с двумя эвристиками готова: перейдём к запросам связности и Краскалу. Строгое исследование стоимости последовательности и обратной функции Аккермана доступно в дополнительных главах.

Для n вершин с номерами 0..n−1 объедините концы всех рёбер edges. Для каждой пары из queries верните True, если вершины в одной компоненте. Используйте DSU; все индексы корректны.

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 connected(5, [[0, 1], [1, 2]], [[0, 2], [0, 3], [4, 4]])Ожидается: [True, False, True]
    Тест 2 connected(3, [], [[0, 1], [2, 2]])Ожидается: [False, True]

    Дополнительные тесты: 4. Проверяются границы и повторы.

    Попыток: 0
    05

    Практическая работа DSU

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Множества и две операции →

    Первый этап — запросы связности и размера: добавьте дороги 0—1, 1—2 и 3—4. Теперь find(0)=find(2), но find(0)≠find(3); size[find(2)]=3. Проверьте эти ответы в модели первой главы. Реализуйте component_sizes в разминке практики, а затем переходите к взвешенным дорогам.

    Обычные примеры: отслеживание компонент связности при добавлении рёбер, построение минимального остовного дерева алгоритмом Краскала, объединение эквивалентных сущностей.

    Для запросов достаточно массивов parent и size со сжатием путей. Теперь второй этап: у дороги есть вес — цена строительства. Цикл — замкнутый путь, который вернулся в начало. Остов — набор дорог, соединяющий все города без циклов; в связном графе из n вершин у него n−1 ребро. Минимальный остов имеет наименьшую сумму весов.

    ВопросОтвет
    Какая цена одиночного find?Не обязана быть O(1) в худшем случае
    Можно ли быстро определить, что u и v связны?Да, find(u) = find(v)
    Можно ли делать split множества?Нет, стандартный DSU не поддерживает
    Можно ли склеить два уже объединённых класса?union не меняет состояние

    Пусть дороги 0—1, 1—2, 0—2, 2—3 имеют веса 1, 2, 3, 4. Берём первые две: они соединяют разные компоненты. Дорогу 0—2 веса 3 пропускаем: путь через 1 уже есть. Дорога 2—3 соединяет последний город. Получен остов веса 1+2+4=7. Это шаги Краскала: рассматривать рёбра от лёгких к тяжёлым и брать только соединяющие разные компоненты.

    Краскал: выберите ребро и решите, брать ли его

    Вес — цена дороги. Остов соединяет все вершины без циклов; на четырёх вершинах ему нужны три ребра. Вершины 0 и 2 после двух первых дорог связаны через 1, поэтому третья дорога замкнёт цикл.

    Компонент: 4. Вес выбранных дорог: 0.

    В Краскале пропускаем ребро, если его концы уже имеют одного представителя: оно замкнуло бы цикл в выбранном лесу. Иначе добавляем вес и объединяем компоненты. Для несвязного графа получится минимальный остовный лес, а не одно дерево; проверяйте число компонент. Для удаления дороги, восстановления пути или поиска минимального ребра на пути одного DSU недостаточно.

    Краскал: минимальный остовный лес

    Сортируем рёбра по весу и принимаем только соединяющие разные компоненты. Результат — сумма весов и число компонент: если оно больше 1, одного остовного дерева нет. Сортировка O(m log(m+1)) доминирует над DSU; память O(n+m). В Rust сумма i64: при данных до 2·10⁵ рёбер и весах до 10⁹ она безопасна.

    Краскал: минимальный остовный лес / Python
    class DSU:
        def __init__(self, n):
            self.parent = list(range(n))
            self.size = [1] * n
            self.components = n
        def find(self, x):
            root = x
            while self.parent[root] != root:
                root = self.parent[root]
            while self.parent[x] != x:
                following = self.parent[x]
                self.parent[x] = root
                x = following
            return root
        def union(self, a, b):
            a, b = self.find(a), self.find(b)
            if a == b:
                return False
            if self.size[a] < self.size[b]:
                a, b = b, a
            self.parent[b] = a
            self.size[a] += self.size[b]
            self.components -= 1
            return True
    def kruskal(n, edges):
        d, total = DSU(n), 0
        for weight, a, b in sorted(edges):
            if d.union(a, b):
                total += weight
        return total, d.components
    
    print(*kruskal(4, [(4, 0, 1), (1, 1, 2), (2, 0, 2), (3, 2, 3)]))
    Ожидаемый вывод
    6 1

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Общие и личные рёбра

    В LeetCode 1579 поддерживаются отдельные DSU для двух пользователей. Сначала обрабатываются общие рёбра: полезное общее соединение экономит отдельные. В конце проверяется связность обеих структур.

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

    Для n вершин и неориентированных рёбер верните размер компоненты каждой вершины. Изолированная вершина имеет размер 1. Используйте DSU и читайте size у представителя.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 component_sizes(6, [[0, 1], [1, 2], [3, 4]])Ожидается: [3, 3, 3, 2, 2, 1]
      Тест 2 component_sizes(3, [])Ожидается: [1, 1, 1]

      Дополнительные тесты: 3. Проверяются границы и повторы.

      Попыток: 0
      А—Я

      Справочник терминов

      Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

      Общий справочник всех лекций ↗

      Найдено: 12 из 12

      СНМ / DSUСистема непересекающихся множеств.
      find / FindВозвращает представитель класса элемента.
      union / UnionОбъединяет два класса.
      makeSet / Make setСоздаёт одноэлементное множество.
      Представитель / RepresentativeВыбранный корневой элемент класса.
      Лес / ForestНабор непересекающихся деревьев указателей.
      Ранг / RankВерхняя оценка глубины дерева для эвристики склейки.
      Объединение по рангу / Union by rankПодвешивание дерева меньшего ранга под дерево большего.
      Path compression / Сжатие путиПеренаправление посещённых ссылок к корню.
      Обратная функция Аккермана / Inverse AckermannОчень медленно растущая функция α.
      Амортизированная оценка / Amortized boundОценка затрат последовательности операций.
      Компонента связности / Connected componentКласс вершин, соединённых путями в графе.
      06

      Ограничения и выбор алгоритма

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Множества и две операции →

      Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.

      ВариантГарантия
      Инициализация parent и sizeO(n) времени и памяти
      Без эвристикОдин find может стоить O(n)
      Только размер или рангO(log n) для одного find
      Размер / ранг вместе со сжатиемO(n+m·α(n)) на инициализацию и m запросов; очень малая амортизированная цена

      Сначала добавляют дороги, затем удаляют произвольную дорогу и спрашивают связность. Достаточно обычного DSU?

      Учитываем сумму размеров групп

      Пусть S — сумма численностей всех групп. Объединяйте каждого участника с первым: O(S) вызовов union. Попарные связи потребовали бы сумму kᵢ², которая может быть огромной даже при S ≤ 500 000. Инициализация массивов стоит O(n).

      Оценка DSU не отменяет сортировку

      Для m рёбер Краскал требует O(m log(m+1)) на сортировку и O(m α(n)) на DSU. Именно сортировка обычно доминирует. При m = 200 000 это несколько миллионов сравнений вместо многократного обхода графа.

      Сумма весов может достигнуть (n−1)·maxWeight. При n = 200 000 и весах до 10⁹ нужен 64-битный тип. Отрицательные веса не мешают Краскалу, а несвязный граф требует явно договориться об ответе: лес или ошибка отсутствия остова.

      Когда обычный DSU не подходит

      Удаление произвольного ребра не выражается обратным union: внутри компоненты может быть много альтернативных путей. Офлайн-удаления иногда обрабатывают в обратном порядке; для откатов нужна другая реализация, обычно без сжатия путей.

      В тестах нужны изолированные вершины, повторное ребро, union внутри одной компоненты, длинная последовательность объединений и повторный find. Проверяйте разбиение, а не конкретный номер корня: корректные реализации выбирают разные представители.

      Определите границы применения DSU

      Задания раздела

      Какие запросы поддерживает обычный DSU без смены структуры?

      В группе 8 человек. Сколько union достаточно, чтобы связать всех с первым?

      Попыток: 0
      07

      Практика: 3 уровня

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Множества и две операции →

      Разминка: реализуйте операции, затем размеры

      Сначала напишите find со сжатием и union по размеру. Затем используйте тот же инвариант для размеров компонент. Проверьте изолированную вершину, повторное ребро и цикл до перехода к внешним задачам.

      Реализуйте find и union по размеру для n вершин 0…n−1. ops содержит ["union",u,v] или ["same",u,v]. Верните ответы только на same. n≥0, индексы корректны. Используйте сжатие путей; граф не храните.

      Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

      Открыто 0 из 3

        5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

        Тест 1 dsu_operations(5, [["union", 0, 1], ["union", 1, 2], ["same", 0, 2], ["same", 0, 3]])Ожидается: [True, False]
        Тест 2 dsu_operations(1, [["same", 0, 0], ["union", 0, 0], ["same", 0, 0]])Ожидается: [True, True]

        Дополнительные тесты: 2. Проверяются границы и повторы.

        Попыток: 0

        Для n вершин и рёбер edges верните размеры компонент всех вершин. После каждого union сохраняйте размер только у представителя; повторы и петли не увеличивают размер. Для n=0 ответ [].

        Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

        Открыто 0 из 3

          5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

          Тест 1 group_sizes(6, [[0, 1], [1, 2], [3, 4]])Ожидается: [3, 3, 3, 2, 2, 1]
          Тест 2 group_sizes(3, [])Ожидается: [1, 1, 1]

          Дополнительные тесты: 2. Проверяются границы и повторы.

          Попыток: 0

          Основное применение

          Начните с Find if Path Exists in Graph и News Distribution. Accounts Merge добавляет отображение строк в id; задачи о циклах и остове используйте после разбора городов и дорог. Трудное испытание в списке необязательно для первого прохода.

          Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.

          Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

          01

          Лёгкие

          Три задачи для закрепления основных приёмов.

          1. Проверить связность по представителям.

            Ограничения. До 2·10⁵ вершин и рёбер.

            Идея и оценка сложности

            Объедините концы каждого ребра, затем сравните find(source) и find(destination). Для source == destination путь уже есть. O(n+mα(n)), память O(n).

          2. Найти лишние рёбра и соединить компоненты.

            Ограничения. До 1000 городов, исходно n−1 дорог.

            Идея и оценка сложности

            Ребро внутри компоненты лишнее: сохраните его. Если компонент c, нужны c−1 новых соединений; столько же лишних рёбер можно заменить. Для курса это первая задача на DSU с выдачей конструкции.

          3. Codeforces · 1167C

            News Distribution

            Объединять группу линейным числом операций.

            Ограничения. n,m и сумма размеров групп до 5·10⁵.

            Идея и оценка сложности

            Каждого участника группы объединяйте с первым, а не со всеми остальными. Затем выдавайте размер представителя каждого пользователя. Пустые группы не создают операций.

          02

          Средние

          Две задачи на сочетание идей и анализ ограничений.

          1. Распознать замыкание цикла очередным ребром.

            Ограничения. До 1000 вершин; дерево дополнено одним ребром.

            Идея и оценка сложности

            Проходите рёбра в исходном порядке. Если концы уже связаны, ребро замыкает цикл; иначе union. Для этой постановки так получается нужное позднее ребро. O(nα(n)).

          2. LeetCode · 721

            Accounts Merge

            Совместить DSU и отображение строк в индексы.

            Ограничения. До 1000 аккаунтов, до 9 адресов на аккаунт; длина строки до 30.

            Идея и оценка сложности

            Общий email связывает аккаунты, одинаковое имя — нет. Храните первый аккаунт каждого адреса, объединяйте повторные. В конце соберите уникальные адреса по корням и отсортируйте; включите сортировку и цену строк в оценку.

          03

          Тяжёлая

          Одна задача для переноса идеи на новую постановку.

          1. Доказать порядок обработки общих рёбер в двух DSU.

            Ограничения. До 10⁵ вершин и 10⁵ рёбер трёх типов.

            Идея и оценка сложности

            Общие рёбра обрабатывайте раньше личных и применяйте к обоим DSU. Затем добавляйте личные, считая действительно нужные соединения. Ответ существует только если обе структуры связны. O((n+m)α(n)).

          Дальше в динамическом программировании граф зависимостей поможет выбирать порядок подзадач. Сливаемые кучи доступны как отдельный продвинутый модуль.

          Проверьте применение DSU после разминки

          Задания раздела

          Краскал уже выбрал 0—1 (вес 1) и 1—2 (вес 2). Следующее 0—2 (вес 3). Что сделать?

          После пропуска ребра 0—2 добавлено 2—3 веса 4. Каков суммарный вес остова?

          Почему повторное объединение не уменьшает число компонент?

          Попыток: 0
          Дополнительные материалыДля углублённого разбора · не влияют на прохождение
          ДОП.

          Анализ только с одной эвристикой

          Мы добавили две эвристики и должны разделить их гарантии. Нельзя объяснять почти постоянное время только словами «дерево стало маленьким»: нужна оценка последовательности операций.

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

          Совместное применение рангов и сжатия путей даёт существенно более сильную амортизированную границу. В лекции анализ строится через классы рангов и многократное пересоединение ссылок.

          T(m, n) = O(m · α(m,n))

          Форма оценки для m операций над n элементами при стандартных предположениях о последовательности операций.

          Почему не просто O(m)?
          Обратная функция Аккермана растёт настолько медленно, что на практически достижимых размерах ведёт себя как маленькая константа, но в строгой асимптотической теории это отдельная функция.

          Без эвристик отдельный find может стоить Θ(n). Только объединение по рангу или размеру даёт O(log n) в худшем случае. Совместно со сжатием получаем O(n + m·α(n)) на инициализацию n элементов и последовательность m запросов в обычной записи оценки. Это не обещание O(1) для каждого отдельного запроса; исходная двухпараметрическая форма α(m,n) ниже описывает ту же теорию при согласованных обозначениях.

          В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

          Выбор по максимальным ограничениям

          Для 200 000 вершин и рёбер нельзя после каждого добавления заново обходить весь граф. DSU оплачивает инициализацию один раз и дальше обрабатывает запросы почти линейно суммарно.

          В формуле появилась α. Разберём её смысл, не подменяя доказательство красивой таблицей больших чисел.

          Исследуйте удвоение размера

          Задания раздела

          Вершина находится в компоненте размера 1. Её глубина выросла 4 раза при union по размеру. Каков минимальный размер новой компоненты?

          Попыток: 0
          ДОП.

          От быстрых функций к обратной функции Аккермана

          Обратная функция Аккермана возникает в строгом учёте повторных подъёмов. Для использования DSU не нужно вычислять огромные значения самой функции; полезнее понять, почему оценка амортизированная и какие предположения в неё входят.

          Обратная функция Аккермана (Inverse Ackermann function) — Крайне медленно растущая функция, возникающая в точном амортизированном анализе DSU.

          Структура доказательства: ранги ограничивают глубины; сжатие ускоряет многократно повторяющиеся переходы; классы, заданные быстро растущей иерархией функций, помогают посчитать общее число проходов через ребра.

          Что значит обратить быстрорастущую функцию
          Иллюстрация ростаЗначение
          x + 15
          2x8
          2ˣ16
          2^(2ˣ)2^16

          Это иллюстрация ускорения роста, не определение функции Аккермана и не вычислитель α(n). Обратная функция отвечает, сколько ступеней роста нужно, чтобы превысить заданную границу.

          Не универсальная константаОбозначения α(n) и α(m,n) не всегда тождественны: выбор нормировки функции и соотношение числа операций m с числом элементов n влияют на форму точной оценки.
          Зачем доказательство сложнее алгоритма
          Сам DSU реализуется несколькими строчками. Сложность возникает при доказательстве, что на длинной последовательности операций дорогостоящие проходы не могут повторяться слишком часто.

          Упрощённая иерархия быстрорастущих функций помогает увидеть идею обращения, но сама по себе не является доказательством оценки DSU. Полное доказательство группирует ранги и считает, сколько раз вершина может переходить к более высокому классу. Для практических размеров α чрезвычайно мала, однако ввод и хранение n элементов всё равно требуют Ω(n) времени и памяти.

          В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

          Не вычисляйте α в решении

          В реализации нужны только массивы и локальные операции. Обратная функция участвует в доказательстве, а не является подпрограммой для каждого find; считать большие числа Аккермана не требуется.

          Теперь достаточно знать интерфейс и область применения. Применим его к компонентам и минимальному остовному лесу.

          Прочитайте амортизированную гарантию

          Задания раздела

          В записи O(n+m·α(n)) что именно оценивается?

          Попыток: 0

          ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

          Промпт для ИИ

          Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.