Zbiory rozłączne Zbiory rozłączne Struktura danych dla zbiorów rozłącznych (ang. disjoint sets data structures), lub krótko zbiory rozłączne, to struktura danych, która dla ustalonego uniwersum przechowuje jego podział na mniejsze, rozłączne zbiory. Struktura danych dla zbiorów rozłącznych pozwala efektywnie realizować następujące operacje: ◦ find(x) – odnalezienie reprezentanta zbioru, do którego należy x; ◦ union(x, y) – połączenie dwóch zbiorów zawierających elementy x i y w jeden zbiór. Czasami rozważa się zbiory rozłączne z dodatkowymi operacjami: ◦ extract(x) – odłączenie elementu x ze zbioru, do którego należy; ◦ append(x) – dodanie do uniwersum nowego elementu x. Implementacja zbiorów rozłącznych Struktura danych dla zbiorów rozłącznych pozwala zarządzać rodziną dynamicznych (zmieniających się w czasie) zbiorów rozłącznych. Uniwersum pamiętamy w tablicy ze wskaźnikami na elementy. Do każdego zbioru odwołujemy się przez jego reprezentanta. Praca ze zbiorami rozłącznymi Pracę ze zbiorami rozłącznymi rozpoczynamy po zainicjalizowaniu struktury: utworzeniu n zbiorów jednoelementowych dla uniwersum rozmiaru n. Czas inicjalizacji wynosi O(n). Następnie wykonujemy na tej strukturze ciąg m operacji union() i find(). Pamięć zajmowana przez strukturę powinna ograniczać się do O(n). Implementacja listowa zbiorów rozłącznych Prostym sposobem implementacji zbiorów rozłącznych jest zapamiętanie każdego zbioru jako listy. Reprezentantem zbioru jest pierwszy element na liście. Każdy element ma dodatkowo bezpośredni wskaźnik do reprezentanta. Implementacja listowa zbiorów rozłącznych Operacje na zbiorach rozłącznych: ◦ find(x) – z węzła x zaglądamy do reprezentanta i odczytujemy jego identyfikator – czas O(1) ◦ union(x, y) gdzie x i y są reprezentantami zbiorów – łączymy dwie listy i poprawiamy wskaźniki do reprezentanta w tej dołączonej – czas liniowy względem jej długości dołączonej listy O(n) Implementacja listowa zbiorów rozłącznych Prosta sztuczka zwana zbalansowanym łączeniem 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 w węźle początkowym listy dodatkowego atrybutu zawierającego rozmiar zbioru oraz adres ostatniego elementu listy). Koszt czasowy wykonania m operacji find() i union() wynosi O(m + n log n); zamortyzowany koszt wykonania pojedynczej operacji union() wynosi O(log n). Implementacja listowa zbiorów rozłącznych Pytanie: Ile razy poszczególny element na liście może zmienić reprezentanta? Odpowiedź: Co najwyżej log(n). Uzasadnienie: Nowy zbiór, do którego będzie należał element jest co najmniej dwa razy większy. Implementacja drzewiasta zbiorów rozłącznych Prostym sposobem implementacji zbiorów rozłącznych jest zapamiętanie każdego zbioru jako drzewa odwrotnie skierowanego (w węzłach trzymamy wskaźniki na rodziców). Reprezentantem zbioru jest korzeń drzewa. Implementacja drzewiasta zbiorów rozłącznych Operacje na zbiorach rozłącznych: ◦ find(x) – z węzła x rozpoczynamy wędrówkę do korzenia – czas O(wysokość drzewa) ◦ union(x, y) gdzie x i y są reprezentantami zbiorów – jedno drzewo podłączamy pod drugie drzewo – O(1) Implementacja drzewiasta zbiorów rozłącznych Sztuczka zwana zbalansowanym łączeniem pozwala zmniejszyć koszt operacji find() w sensie najgorszego przypadku: polega ona na tym, że podczas operacji union() zawsze dołączamy niższe drzewo pod wyższe (wymaga to przechowywania w korzeniu drzewa dodatkowego atrybutu zawierającego wysokość krawędziową drzewa – rank). Koszt czasowy wykonania m operacji find() i union() wynosi O(m log n); koszt wykonania pojedynczej operacji find() wynosi O(log n), ponieważ wysokość drzewa jest ograniczona przez logarytm z liczby węzłów w drzewie. Implementacja drzewiasta zbiorów rozłącznych Pytanie: Ile razy poszczególny zbiór może być dołączany do innego zbioru? Odpowiedź: Co najwyżej log(n). Uzasadnienie: Nowy zbiór, do którego będą należały elementy podłączanego zbioru na co najmniej wykładniczą w stosunku do wysokości drzewa liczbę elementów. Implementacja drzewiasta zbiorów rozłącznych Sztuczka zwana kompresją ścieżek pozwala zmniejszyć głębokość części elementów po operacji find(): polega ona na tym, że podczas operacji find() zawsze dołączamy odwiedzone (leżące na ścieżce od danego węzła do korzenia) węzły do korzenia drzewa. Koszt czasowy wykonania m operacji find() i union() wynosi O(m log* n); koszt wykonania pojedynczej operacji find() wynosi w najgorszym przypadku O(log n). Zadanie 1 Zdefiniuj drzewiastą strukturę dla zbiorów rozłącznych. Przetestuj swoją strukturę dla losowego ciągu operacji union() i find(). Zadanie 2 Zdefiniuj drzewiastą strukturę dla zbiorów rozłącznych. Zaimplementuj w swojej strukturze zbalansowane łączenie przy union() i kompresję ścieżek przy find(). Przetestuj swoją strukturę dla losowego ciągu operacji union() i find().