Podstaw¸a pomyślnego rozwi¸azania każdego zadania jest wymy- Literatura [WDA] - 3 [ASP] - 4 ślenie odpowiedniego algorytmu. W przypadku podjęcia dobrej
de-cyzji, rozwi¸azanie zadania jest już tylko kwesti¸a czasu. Niestety, podczas zawodów, trzeba się liczyć dodatkowo ze ścisłymi ograni-czeniami czasowymi. W takiej sytuacji nie tylko algorytm, ale
rów-nież i sposób implementacji jest niezwykle ważny. Rozwi¸azanie wielu zadań wymaga użycia różnego rodzaju struktur danych. Zazwyczaj istnieje możliwość wyboru pomiędzy bardziej efektywn¸a metod¸a lecz trudniejsz¸a do zaimplementowania oraz wolniejsz¸a i prostsz¸a. Bard-zo ważn¸a decyzj¸a projektow¸a jest wybór odpowiedniej struktury danych, która jest wystar-czaj¸aco efektywna, aby rozwi¸azanie zmieściło się w limitach czasowych przygotowanych przez organizatorów, a jednocześnie jej implementacja była jak najprostsza. Jest to niezwykle waż-na umiejętność, któr¸a waż-nabywa się z czasem. Najlepsz¸a decyzj¸a jest zazwyczaj po prostu użycie
— o ile to możliwe, gotowej struktury danych z biblioteki STL. Umiejętne wykorzystanie tej biblioteki pozwala skrócić długość implementowanego programu niekiedy i kilkukrotnie.
W niektórych sytuacjach okazuje się jednak, że struktury danych udostępnione przez bib-liotekę STL s¸a niewystarczaj¸ace. W niniejszym rozdziale przedstawimy implementację kilku najczęściej wykorzystywanych podczas zawodów struktur danych, które nie s¸a udostępnione w bibliotece STL.
0
Rysunek 6.1: (a) Reprezentacja struktury zbiorów rozł¸acznych {0, 1, 3}, {2}, {4}, {5}. (b) Stan struk-tury danych po wykonaniu operacji zł¸aczenia zbiorów {2} i {4}. (c) Stan strukstruk-tury po zł¸aczeniu zbiorów {0, 1, 3} i {2, 4}. (d) Wyszukanie zbioru, do którego należy element 4 spowodowało skompresowanie ścieżki od elementu 4 do korzenia jego drzewa.
6.1. Struktura danych do reprezentacji zbiorów rozł¸acznych
Załóżmy, że mamy dane n rozł¸acznych zbiorów jednoelemen- Literatura [WDA] - 22
[ASD] - 4 towych — {0}, {1}, . . . , {n − 1}. Chcielibyśmy wykonywać na tych
zbiorach dwie operacje: ł¸aczenie ze sob¸a dwóch zbiorów oraz wyz-naczanie zbioru, do którego należy dany element k ∈ {0, 1, . . . , n −
1}.Jednym ze sposobów rozwi¸azania tego zadania jest przypisanie każdej liczbie k numeru zbioru, do którego ona należy (na pocz¸atku liczbie k przypisywany jest zbiór o numerze k). Sprawdzenie, do którego zbioru należy dany element sprowadza się wtedy do zwrócenia numeru przypisanego zbioru. Zł¸aczenie dwóch zbiorów P i Q natomiast można zrealizować poprzez zamianę numeru p wszystkich elementów zbioru P na numer zbioru Q. Widać, że wykonanie zł¸aczenia dwóch zbiorów wymaga wyznaczenia wszystkich elementów należ¸acych do zł¸aczanych zbiorów, zatem operacja ta ma pesymistycznie złożoność liniow¸a — nie jest to zatem rozwi¸azanie szczególnie efektywne.
Efektywn¸a, a zarazem dość prost¸a struktur¸a danych, pozwalaj¸ac¸a na wykonywanie powyż-szych dwóch operacji, jest tzw. struktura Find and Union. Struktura ta reprezentuje zbiory w postaci lasu, w którym wierzchołkami s¸a elementy ze zbioru {0, 1, . . . , n − 1}. Każdy zbiór jest reprezentowany jako drzewo, w którym każdy element przechowuje numer swojego oj-ca. Operacja ł¸aczenia dwóch zbiorów polega na dodaniu krawędzi między korzeniami drzew reprezentuj¸acych ł¸aczone zbiory, natomiast operacja wyznaczania zbioru, do którego należy element k — na znalezieniu numeru korzenia drzewa, do którego należy element k. W ten sposób dwa elementy należ¸a do tego samego zbioru, jeśli maj¸a wspólny korzeń w drzewie.
Zamortyzowana złożoność wykonania m operacji na tej strukturze danych to O(m ∗ log∗(m)), gdzie log∗ jest odwrotności¸a funkcji Ackermana. Funkcja ta bardzo wolno rośnie (istotnie wolniej od logarytmu) — w praktycznych zastosowaniach można zakładać zatem, że złożoność wykonania m operacji to po prostu O(m)). Uzyskanie takiej złożoności jest możliwe dzięki wykorzystaniu dwóch metod:
podczas wyszukiwania korzenia drzewa, do którego należy element k, ścieżka do ko-rzenia ulega kompresji — wszystkie wierzchołki, leż¸ace na ścieżce między k a jego korzeniem, zostaj¸a bezpośrednio poł¸aczone z korzeniem. W ten sposób wykonywanie operacji wyszukiwania powoduje skracanie ścieżek w drzewach, dzięki czemu kolejne wyszukiwania będ¸a wykonywać się szybciej.
operacja ł¸aczenia dwóch drzew reprezentuj¸acych zbiory realizowana jest według rang,
co sprowadza się do przył¸aczania mniejszego drzewa do korzenia drzewa większego, a nie na odwrót. Takie podejście minimalizuje sumaryczn¸a długość powstaj¸acych ścieżek.
Przykładowy sposób realizacji ł¸aczenia i wyszukiwania zbiorów został przedstawiony na ry-sunku 6.1.
Implementacja struktury FAU jest przedstawiona na listingu 6.1. Operacja Union(x,y) powoduje zł¸aczenie zbiorów zawieraj¸acych elementy x oraz y, natomiast operacja Find(x) na wyznacza numer zbioru, do którego należy element x. Na listingu 6.2 przedstawiony jest sposób działania tej struktury dla 6-elementowago zbioru oraz sekwencji przykładowych op-eracjiFindiUnion.
Listing 6.1: Implementacja strukturyFAU // Struktura danych do reprezentacji zbiorów rozłącznych 01 struct FAU {
02 int*p, *w;
// Konstruktor tworzący reprezentację n jednoelementowych zbiorów rozłącznych 03 FAU(intn) : p(new int[n]), w(new int[n]) {
04 REP(x, n) p[x] = w[x] = -1;
05 }
// Destruktor zwalniający wykorzystywaną pamięć 06 ∼FAU() {
07 delete[]p;
08 delete[]w;
09 }
// Funkcja zwraca numer reprezentanta zbioru, do którego należy element x 10 intFind(intx) {
11 return (p[x] < 0) ? x : p[x] = Find(p[x]);
12 }
// Funkcja łączy zbiory zawierające elementy x oraz y 13 voidUnion(intx, int y) {
14 if ((x = Find(x)) == (y = Find(y))) return;
15 if (w[x] > w[y]) p[y] = x;
16 else p[x] = y;
17 if (w[x] == w[y]) w[y]++;
18 } 19};
Listing 6.2: Przykład działania strukturyFAU.
Zlaczenie zbiorow zawierajacych elementy 0 i 1
Find(0) = 1 Find(1) = 1 Find(2) = 2 Find(3) = 3 Find(4) = 4 Find(5) = 5 Zlaczenie zbiorow zawierajacych elementy 3 i 4
Find(0) = 1 Find(1) = 1 Find(2) = 2 Find(3) = 4 Find(4) = 4 Find(5) = 5 Zlaczenie zbiorow zawierajacych elementy 1 i 5
Find(0) = 1 Find(1) = 1 Find(2) = 2 Find(3) = 4 Find(4) = 4 Find(5) = 1 Zlaczenie zbiorow zawierajacych elementy 0 i 5
Find(0) = 1 Find(1) = 1 Find(2) = 2 Find(3) = 4 Find(4) = 4 Find(5) = 1
Listing 6.3: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 6.2. Pełny kod źródłowy programu znajduje się w pliku fau.cpp
01 intmain() {
02 int n, m, e1, e2;
// Wczytaj liczbę elementów oraz operacji do wykonania
03 cin >> n >> m;
04 FAU fau(n);
05 REP(x, m) {
// Wczytaj numery dwóch elementów oraz złącz zbiory je zawierające
06 cin >> e1 >> e2;
07 fau.Union(e1, e2);
08 cout << "Zlaczenie zbiorow zawierajacych elementy " <<
09 e1 << " i " << e2 << endl;
10 REP(y, n) cout << "Find(" << y << ") = " << fau.Find(y) << " ";
11 cout << endl;
12 }
13 return 0;
14}