Dwa operacje, jedna las
Struktura zbiorów rozłącznych (union-find) utrzymuje zbiór elementów podzielony na nieprzecinające się grupy i szybko odpowiada na dwie pytania: find(x) - w której grupie jest x? - oraz union(x, y) - łączy grupę x z grupą y. Internale każda grupa to drzewo, a każdy element przechowuje tylko wskaźnik do swojego rodzica. Węzeł drzewa jest kanonicznym reprezentantem grupy: dwa elementy są w tej samej grupie dokładnie kiedy ich korzenie pasują.
Algorytm Kruskala dla minimalnego drzewa rozpinającego to klasyczny przypadek użycia: sortuje krawędzie pod względem wag, a następnie przechodzi po nich w rosnącej kolejności i dodaje krawędź do drzewa, chyba że jej dwie końce są już połączone - pytanie, na które union-find odpowiada w czasie potrzebnym do przejścia dwóch krótkich łańcuchów do ich korzeni. Taka sama struktura śledzi komponenty spójne podczas przybywania krawędzi po jednej, wykrywa cykle w grafie i łączy regiony w segmentacji obrazu.
Proste rozwiązanie i dlaczego się zdegeneruje
Pierwsza próba polegała na tym, że find przeszuka węzły rodzicielskie do korzenia, a union łączył jeden korzeń pod drugim losowo. To działa, ale nic nie zapobiega temu, aby drzewo stało się długą listą - union elementy w zwiększonym porządku, zawsze łącząc nowy korzeń pod poprzednim, i otrzymujesz proste liniowe n węzłów. Find kosztuje wtedy O(n) zamiast O(log n) lub lepszego czasu, który na самом fakcie chcesz uzyskać.
function find(x) {
while (parent[x] !== x) x = parent[x];
return x;
}
function union(x, y) {
const rx = find(x), ry = find(y);
if (rx !== ry) parent[rx] = ry; // arbitrary attachment - can chain badly
}
Unia według rangi
Rozwiązanie polega na zawsze przypinaniu drzewa o mniejszej głębokości pod drzewo o większej głębokości. Podczas śledzenia śledź albo dokładną wielkość, albo górne ograniczenie rangi (w przybliżeniu, wysokości drzewa) dla każdego korzenia; podczas łączenia, korzeń o mniejszej rangi staje się dzieckiem korzenia o większej rangi, a remisy zwiększają rangę zwycięzcy o jeden. To same ogranicza wysokość każdego drzewa do O(log n), ponieważ drzewo o rangi r może być utworzone tylko przez łączenie dwóch drzew o rangi r-1, więc potrzebne jest co najmniej 2 do potęgi r elementów, aby osiągnąć rangę r.
Kompresja ścieżki: przysiłanie podczas przebiegu
Unia według rang ogranicza wysokość, ale można zrobić znacznie lepsze przez ponowne zapisywanie historii przy każdym przejściu do korzenia. Kompresja ścieżki sprawia, że każdy węzeł odwiedzony podczas operacji find() wskazuje bezpośrednio na korzeń po znalezieniu tego korzenia, co oznacza, że kolejne operacje find() z tych węzłów są O(1).
```function find(x) { if (parent[x] !== x) parent[x] = find(parent[x]); // przysiłanie bezpośrednio do korzenia return parent[x]; }``` Tarjan i van Leeuwen udowodnili, że kombinacja unii według rang z kompresją ścieżki prowadzi do kosztu amortyzowanego dowolnej sekwencji m operacji na n elementach dążącego do O(m · α(n)), gdzie α jest odwrotnością szybko rosnącej funkcji Ackermann. Dla każdego praktycznego n, α(n) wynosi co najwyżej 4 lub 5, co oznacza, że struktura zachowuje się tak, jakby każda operacja była O(1) - rzadki przypadek, w którym prawdziwa granica niekonstancja jest nierozróżnialna od czasu stałego na danych rzeczywistych.
function find(x) {
if (parent[x] !== x) parent[x] = find(parent[x]); // point straight at the root
return parent[x];
}
Często zadawane pytania
Jak różni się algorytm union-find od przeszukiwania grafu?
BFS lub DFS odpowiada na pytania dotyczące połączonego grafu w czasie O(V+E) dla każdego zapytania, a potrzebuje całego grafu w pamięci. Union-find przetwarza krawędzie jedna po drugiej, gdy one przychodzą, odpowiada na zapytania o należenie do grupy w bliskim stałym czasie amortyzowanym i nigdy nie odwiedza krawędzi ponownie raz, gdy one zostały połączone - idealnie nadaje się dla grafów strumieniowych lub algorytmu Kruskala.
Dlaczego funkcja odwrotna Ackermann'a jest tak wolno rosnąca?
Bo odwraca funkcję Ackermann'a, która rośnie szybciej niż dowolna wieżę wykładniczą. Zatem jej odwrotność rośnie niezwykle wolno - pozostaje poniżej 5 dla każdego n, którego można przechować na rzeczywistym komputerze, co jest powodem traktowania union-find z obu optymalizacji jako prawie stałą czasową w praktyce.
Czy muszę użyć zarówno rank-union, jak i ścieżki kompresji?
Jedna z tych optymalizacji sama już daje dobry ograniczający czas O(log n) na operację. Złączenie obu spada do amortyzowanego kosztu O(alpha(n)), funkcja odwrotna Ackermann'a. W większości implementacji używa się obu, ponieważ każda z nich jest prawie bezpłatna do dodania na topie drugiej.
Wypróbuj na żywo
Wszystko powyżej działa bezpośrednio w Twojej przeglądarce — otwórz Union-Find — Disjoint Sets & Path Compression i zmieniaj parametry podczas działania. Nic nie jest instalowane ani przesyłane na serwer, cały model działa w jednej karcie.
▶ Otwórz symulację Union-Find — Disjoint Sets & Path Compression