← Все вопросы

Как реализовать DSU со сжатием путей и объединением по рангу, чтобы получить почти O(1)?

Задан 20 месяцев назад1.1к просмотров2 ответа
11

Хочу каноничную реализацию системы непересекающихся множеств для Краскала и проверки связности. Знаю, что есть две оптимизации — сжатие путей и объединение по рангу, но не понимаю, как их аккуратно совместить и почему именно вместе они дают почти константу. Можно полный рабочий код на C++?

2 ответа

17
✓ Принятый ответ — помог автору

DSU держит лес деревьев: каждый элемент знает своего «родителя», корень — представитель множества. Две оптимизации:

  1. Сжатие путей в find: по пути к корню перевешиваем все вершины напрямую на корень.
  2. Объединение по рангу (приближённая высота): меньшее дерево подвешиваем под большее, чтобы не растить глубину.
struct DSU {
    vector<int> p, rnk;
    DSU(int n) : p(n), rnk(n, 0) { iota(p.begin(), p.end(), 0); }
    int find(int x) {
        while (x != p[x]) x = p[x] = p[p[x]]; // path halving
        return x;
    }
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;
        if (rnk[a] < rnk[b]) swap(a, b);
        p[b] = a;
        if (rnk[a] == rnk[b]) ++rnk[a];
        return true;
    }
};

Я использовал path halving (p[x] = p[p[x]]) — он без рекурсии и без второго прохода, амортизированно так же хорош, как полное сжатие. С обеими оптимизациями амортизированная сложность одной операции — O(α(n)), где α — обратная функция Аккермана (для всех реальных n ≤ 4). По памяти O(n). unite возвращает false, если уже в одном множестве — удобно для подсчёта числа объединений в Краскале.

6

Частая ошибка — рекурсивный find со сжатием на больших n:

int find(int x){ return x==p[x] ? x : p[x]=find(p[x]); }

Красиво, но на цепочке из 10^6 элементов первый вызов уйдёт в рекурсию глубиной 10^6 и упадёт по стеку (RE/segfault), особенно если рёбра подавали в «плохом» порядке. На олимпиаде безопаснее итеративный вариант с path halving из принятого ответа. Если очень хочется полное сжатие — делайте два прохода циклом: сначала найдите корень, потом пройдите путь ещё раз и перевесьте всех на него.

Ваш ответ

Войдите, чтобы ответить на вопрос.