• Nie Znaleziono Wyników

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}