Algorytmy i struktury danych/Find

Transkrypt

Algorytmy i struktury danych/Find
Algorytmy i struktury danych/Find-Union
Sumowanie zbiorów rozłącznych
Niekiedy przydatne okazuje się grupowanie elementów w rozłączne zbiory. Struktura danych dla
zbiorów rozłącznych pozwala zarządzać taką rodziną dynamicznych (tzn. zmieniających się w
czasie) zbiorów rozłącznych. Do każdego zbioru odwołujemy się przez jego reprezentanta
(zazwyczaj jest to jakiś element tego zbioru), przy czym wymagamy, żeby dwa zapytania o
reprezentanta danego zbioru dawały taki sam wynik, jeśli w międzyczasie sam zbiór się nie
zmieniał. Do dyspozycji mamy trzy operacje:
• MakeSet(x): tworzy nowy jednoelementowy zbiór { x }, którego reprezentantem jest
oczywiście x (zakładamy, że x nie należy do żadnego innego zbioru w rodzinie);
• Find(x): zwraca reprezentanta zbioru, do którego należy x;
• Union(x,y): łączy dwa zbiory, których reprezentantami są x i y, w nowy zbiór będący ich
teoriomnogościową sumą; dwa pierwotne zbiory (zakładamy, że były różne) są przy tym
niszczone.
W naszych rozważaniach dotyczących kosztów powyższych operacji w różnych implementacjach
struktury danych dla zbiorów rozłącznych będziemy brali pod uwagę dwa parametry: n, czyli łączną
liczbę operacji MakeSet (bez straty ogólności można założyć, że wszystkie te operacje są
wykonywane na początku), oraz m, czyli łączną liczbę wszystkich operacji MakeSet, Find i Union
(w szczególności mamy
). Nietrudno zauważyć, że liczba operacji Union nie jest większa niż
n-1 (bo każda z nich zmniejsza liczbę zbiorów w rodzinie o jeden).
Większość zastosowań struktur danych dla zbiorów rozłącznych sprowadza się do zarządzania
pewną dynamicznie rozrastającą się relacją równoważności (jej klasy abstrakcji to nasze zbiory
rozłączne). Jako przykłady można wymienić:
• algorytm Kruskala znajdowania minimalnego drzewa rozpinającego;
• rozpoznawanie spójnych składowych w dynamicznie rozrastającym się grafie
nieskierowanym;
• generowanie labiryntów;
• rozpoznawanie obszarów na obrazach w postaci cyfrowej.
Implementacja listowa
Oto prosta implementacja struktury danych dla zbiorów rozłącznych: Każdy zbiór jest
reprezentowany jako lista swoich elementów. Reprezentantem zbioru jest pierwszy element na
liście. Każdy element ma dodatkowo bezpośredni wskaźnik do reprezentanta, dzięki czemu koszt
operacji Find to O(1). Operacja MakeSet jest również bardzo prosta i polega na utworzeniu
jednoelementowej listy.
Bardziej kłopotliwa jest operacja Union: wprawdzie koszt połączenia dwóch list jest stały, ale
trzeba jeszcze we wszystkich elementach listy, która jest dołączana na koniec drugiej, uaktualnić
wskaźnik do reprezentanta, co zajmuje czas liniowy względem jej długości. Prosta sztuczka zwana
heurystyką łączenia z wyważaniem pozwala zmniejszyć koszt operacji Union (w sensie
zamortyzowanym). Polega ona na tym, że podczas operacji Union zawsze dołączamy krótszą listę
na koniec dłuższej (wymaga to przechowywania wraz z listą dodatkowego atrybutu "rozmiar").
Jako ćwiczenie pozostawiamy dowód faktu, że teraz koszt wykonania m operacji MakeSet, Find i
Union, spośród których n to MakeSet, wynosi
.
Implementacja drzewiasta
Alternatywna implementacja to las zbiorów rozłącznych, w którym każde drzewo odpowiada
jednemu ze zbiorów z naszej rodziny. Z każdym elementem zbioru jest związany wskaźnik do
pewnego innego elementu (jego ojca w drzewie). Wyjątek stanowi reprezentant zbioru (a zarazem
korzeń drzewa), którego wskaźnik wskazuje na niego samego. Operacja MakeSet tworzy
jednoelementowe drzewo z korzeniem wskazującym na samego siebie. Operacja Union polega na
przestawieniu wskaźnika jednego reprezentanta tak, by wskazywał na drugiego. Koszt obydwu tych
operacji to oczywiście O(1).
Operacja Find polega na przejściu po wskaźnikach ścieżki od danego węzła do reprezentanta (czyli
korzenia). Jej koszt jest proporcjonalny do głębokości danego węzła w drzewie. Nietrudno podać
przykład ciągu operacji, który powoduje powstanie drzewa w kształcie listy, czyli pesymistyczny
koszt takiej operacji Find to
dla rodziny zbiorów zawierającej łącznie n elementów.
Łączenie według wysokości
Podobną do stosowanego dla list "łączenia z wyważaniem" metodą można zapewnić lepsze
ograniczenie kosztu operacji Find. W korzeniu każdego drzewa przechowujemy dodatkowy atrybut
- jego wysokość - i podczas operacji Union zawsze przyłączamy niższe drzewo do wyższego (remisy
rozstrzygając dowolnie). Koszt Union pozostaje stały, a nietrudno pokazać, że teraz wysokość
drzewa jest co najwyżej logarytmiczna względem jego rozmiaru, skąd wynika, że pesymistyczny
koszt operacji Find to
.
Kompresja ścieżki
Równie prosta, a bardzo skuteczna heurystyka polega na tym, że w ramach operacji Find po
wyznaczeniu korzenia v drzewa zawierającego węzeł x przebiegamy ponownie ścieżkę od x do v i
przyłączamy wszystkie napotkane węzły bezpośrednio do v. Dzięki temu późniejsze operacje Find
dotyczące węzłów na ścieżce od x do v oraz ich potomków będą kosztowały mniej.
Przy stosowaniu kompresji ścieżki utrzymywanie poprawnych wartości atrybutu "wysokość"
wykorzystywanego podczas wykonywania Union byłoby zbyt kosztowne. Zamiast tego będziemy
przechowywać w węzłach atrybut "ranga", który będziemy wykorzystywać i modyfikować
dokładnie tak samo, jak atrybut "wysokość" przy łączeniu według wysokości: podczas operacji
Union przyłączamy drzewo o niższej randze do drzewa o wyższej randze, a w przypadku remisu po
przyłączeniu zwiększamy rangę korzenia o 1. Tak więc ranga stanowi górne ograniczenie
wysokości drzewa, a w myśl zadania 3 jej wartość nie przekracza
dla rodziny zbiorów
zawierającej łącznie n elementów. Poniższy lemat wylicza kilka dalszych użytecznych własności
rang:
LEMAT LEMAT 1
W lesie zbiorów rozłącznych z łączeniem według rangi i kompresją ścieżek
(a) ranga węzła, który nie jest korzeniem, jest mniejsza (ściśle) niż ranga jego ojca;
(b) zawsze kiedy ojciec danego węzła się zmienia, to nowy ojciec ma większą rangę niż stary;
(c) jeśli ranga reprezentanta pewnego zbioru jest równa r, to rozmiar tego zbioru wynosi co
najmniej ;
(d) jeśli zbiory zawierają łącznie
dowolnego
.
elementów, to jest co najwyżej
węzłów rangi r, dla
Dowód lematu pozostawiamy jako ćwiczenie.
Zanim przystąpimy do analizy kosztu opisanych wyżej operacji, zdefiniujemy dwie funkcje: jedną
bardzo szybko rosnącą i drugą, rosnącą bardzo wolno. Niech
,
dla k>0.
Oto kilka pierwszych wartości funkcji F:
k
0 1 2
3
4
F(k)
1 2 4
16
65536
Jak widać, funkcja F rośnie bardzo szybko.
Funkcję logarytm iterowany definiuje się następująco:
tabelka początkowych wartości funkcji G:
5
n
0..1 2 3..4 5..16 17..65536
G(n) 0 1 2
3
4
5
Funkcja G rośnie niesłychanie wolno i dla wszystkich n mających jakiekolwiek praktyczne
znaczenie nie przekracza wartości 5.
. Oto
Na potrzeby analizy kosztu zamortyzowanego operacji na lesie zbiorów rozłącznych
przyporządkujemy każdy węzeł do jednego z ponumerowanych bloków: jeśli mianowicie dany
węzeł ma rangę r, to przyporządkowujemy go do bloku o numerze
. Ponieważ rangi węzłów
mieszczą się w zakresie od 0 do
, mamy
bloków o numerach od 0 do
. Możemy juz udowodnić
TWIERDZENIE 2 (HOPCROFT, ULLMAN [HU])
W lesie zbiorów rozłącznych z łączeniem według rangi i kompresją ścieżki koszt m operacji,
spośród których n to operacje MakeSet, wynosi
.
Dowód: Ponieważ operacje MakeSet i Union mają koszt stały, wystarczy oszacować łączny koszt m
operacji Find. Koszt operacji Find(x) jest proporcjonalny do liczby węzłów na ścieżce od x do
korzenia (przed kompresją). Rozbijemy koszt każdej takiej operacji na składniki, które
zaksięgujemy do różnych pul. Sumując pule, dostaniemy łączny koszt wszystkich operacji Find.
Dla wierzchołka v leżącego na ścieżce od x do korzenia:
Przypadek 1: Jeśli v lub jego ojciec jest korzeniem, to jednostkę kosztu księgujemy do puli
KONIEC_ŚCIEŻKI;
Przypadek 2: Jeśli v i ojciec v są w różnych blokach, to jednostkę kosztu księgujemy do puli
ZMIANA_BLOKU;
Przypadek 3: Jeśli v i ojciec v sa w tym samym bloku, to jednostkę kosztu księgujemy do puli
TEN_SAM_BLOK.
Przypadek 1 dla każdej operacji Find występuje co najwyżej dwukrotnie, więc łączna zawartość
puli KONIEC_ŚCIEŻKI to co najwyżej 2m. Podczas pojedynczej operacji Find przypadek 2 może
wystąpić co najwyżej
razy (bo jest nie więcej niż
różnych bloków, a w myśl lematu
1(a) rangi na ścieżce do korzenia tworzą ciąg rosnący). Zatem łączna zawartość puli
ZMIANA_BLOKU to
.
Pozostaje nam zatem oszacować zawartość puli TEN_SAM_BLOK. Jeśli v jest w bloku o numerze
, przypadek 3 może dla niego zajść co najwyżej
razy (wynika to z lematu
1(b): ranga ojca v za każdym razem rośnie, a kiedy v i ojciec v znajdą się w różnych blokach, to dla
v już do końca będzie zachodził przypadek 2 lub 1). Dla v należącego do bloku
przypadek 3
może zajść co najwyżej raz.
Na mocy lematu 1(d) liczbę węzłów w bloku
można oszacować z góry przez
skąd łączny wkład do puli TEN_SAM_BLOK pochodzący od węzłów z bloku
szacuje się z
góry przez
(to ostatnie oszacowanie jest prawdziwe również dla
). Sumując po g, dostajemy górne oszacowanie zawartości puli TEN_SAM_BLOK
.
Uwaga: Wynikające z powyższego twierdzenia oszacowanie kosztu zamortyzowanego operacji
Find
można poprawić! Robert Tarjan [T1,T2] podał asymptotycznie optymalne
oszacowanie tego kosztu przez tzw. funkcyjną odwrotność funkcji Ackermanna, rosnącą jeszcze
wolniej niż funkcja . Przystępny opis górnego oszacowania (chociaż znacznie bardziej
skomplikowany niż powyższy dowód) można znaleźć w książce [CLRS].

Podobne dokumenty