Michał Kazimierz Kowalczyk rok 1, semestr 2 nr albumu indeksu: Algorytmy i struktury danych. Problem połączeń

Podobne dokumenty
Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 9. Karol Tarnowski A-1 p.

Strategia "dziel i zwyciężaj"

Lekcja : Tablice + pętle

Podstawy programowania 2. Temat: Drzewa binarne. Przygotował: mgr inż. Tomasz Michno

Drzewa rozpinajace, zbiory rozłaczne, czas zamortyzowany

Algorytm selekcji Hoare a. Łukasz Miemus

Otrzymaliśmy w ten sposób ograniczenie na wartości parametru m.

Algorytmy i Struktury Danych. Anna Paszyńska

Złożoność obliczeniowa algorytmu ilość zasobów komputera jakiej potrzebuje dany algorytm. Pojęcie to

Wyszukiwanie binarne

Algorytmy i struktury danych

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

Optymalizacja. Przeszukiwanie lokalne

Technologie cyfrowe. Artur Kalinowski. Zakład Cząstek i Oddziaływań Fundamentalnych Pasteura 5, pokój 4.15

Algorytmy i struktury danych. Drzewa: BST, kopce. Letnie Warsztaty Matematyczno-Informatyczne

Konkurs z przedmiotu eksploracja i analiza danych: problem regresji i klasyfikacji

Drzewa poszukiwań binarnych

OSTASZEWSKI Paweł (55566) PAWLICKI Piotr (55567) Algorytmy i Struktury Danych PIŁA

Algorytmy sortujące i wyszukujące

Porównanie czasów działania algorytmów sortowania przez wstawianie i scalanie

Język C : programowanie dla początkujących : przewodnik dla adeptów programowania / Greg Perry, Dean Miller. Gliwice, cop

Problem 1 prec f max. Algorytm Lawlera dla problemu 1 prec f max. 1 procesor. n zadań T 1,..., T n (ich zbiór oznaczamy przez T )

Zasady analizy algorytmów

Pętle. Dodał Administrator niedziela, 14 marzec :27

Poprawność semantyczna

Przypomnij sobie krótki wstęp do teorii grafów przedstawiony na początku semestru.

Zadanie 1: Piętnastka

Porównanie wydajności CUDA i OpenCL na przykładzie równoległego algorytmu wyznaczania wartości funkcji celu dla problemu gniazdowego

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

Analiza algorytmów zadania podstawowe

Algorytmy mrówkowe (optymalizacja kolonii mrówek, Ant Colony optimisation)

Wstęp do programowania

Programowanie w VB Proste algorytmy sortowania

Algorytmy w teorii liczb

Informatyka 1. Złożoność obliczeniowa

Zadanie 1: Piętnastka - uzupełnienie

Efektywna metoda sortowania sortowanie przez scalanie

Drzewa poszukiwań binarnych

Teraz bajty. Informatyka dla szkół ponadpodstawowych. Zakres rozszerzony. Część 1.

Egzamin, AISDI, I termin, 18 czerwca 2015 r.

Programowanie proceduralne INP001210WL rok akademicki 2017/18 semestr letni. Wykład 3. Karol Tarnowski A-1 p.

Algorytmy i str ruktury danych. Metody algorytmiczne. Bartman Jacek

Wykład 3. Metoda dziel i zwyciężaj

Algorytmy równoległe: ocena efektywności prostych algorytmów dla systemów wielokomputerowych

Wykład 6. Wyszukiwanie wzorca w tekście

Definicja. Ciąg wejściowy: Funkcja uporządkowująca: Sortowanie polega na: a 1, a 2,, a n-1, a n. f(a 1 ) f(a 2 ) f(a n )

TEORETYCZNE PODSTAWY INFORMATYKI

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Algorytmy i. Wykład 5: Drzewa. Dr inż. Paweł Kasprowski

Zadania do wykonania. Rozwiązując poniższe zadania użyj pętlę for.

Plan wykładu. Przykład. Przykład 3/19/2011. Przykład zagadnienia transportowego. Optymalizacja w procesach biznesowych Wykład 2 DECYZJA?

Podstawy Programowania. Złożoność obliczeniowa

Struktury danych i złożoność obliczeniowa Wykład 7. Prof. dr hab. inż. Jan Magott

Wykład X. Programowanie. dr inż. Janusz Słupik. Gliwice, Wydział Matematyki Stosowanej Politechniki Śląskiej. c Copyright 2016 Janusz Słupik

Algorytmy i Struktury Danych.

Wyrażenie include(sciezka_do_pliku) pozwala na załadowanie (wnętrza) pliku do skryptu php. Plik ten może zawierać wszystko, co może się znaleźć w

ALGORYTMY I STRUKTURY DANYCH

Funkcja liniowa - podsumowanie

Matematyczne Podstawy Informatyki

Podstawy programowania w języku C

Wstęp do programowania. Dziel i rządź. Piotr Chrząstowski-Wachtel

Algorytmy i struktury danych. Co dziś? Tytułem przypomnienia metoda dziel i zwyciężaj. Wykład VIII Elementarne techniki algorytmiczne

W przeciwnym wypadku wykonaj instrukcję z bloku drugiego. Ćwiczenie 1 utworzyć program dzielący przez siebie dwie liczby

Badania Operacyjne Ćwiczenia nr 5 (Materiały)

Część I. Uwaga: Akceptowane są wszystkie odpowiedzi merytorycznie poprawne i spełniające warunki zadania. Zadanie 1.1. (0 3)

Marcel Stankowski Wrocław, 23 czerwca 2009 INFORMATYKA SYSTEMÓW AUTONOMICZNYCH

Zaawansowane algorytmy i struktury danych

Struktury danych i złożoność obliczeniowa Wykład 2. Prof. dr hab. inż. Jan Magott

Algorytmy stochastyczne, wykład 02 Algorytmy genetyczne

Sieci Mobilne i Bezprzewodowe laboratorium 2 Modelowanie zdarzeń dyskretnych

Podstawowe algorytmy i ich implementacje w C. Wykład 9

Rekurencje. Jeśli algorytm zawiera wywołanie samego siebie, jego czas działania moŝe być określony rekurencją. Przykład: sortowanie przez scalanie:

Języki i techniki programowania Ćwiczenia 2

Podstawy programowania skrót z wykładów:

Równoległy algorytm wyznaczania bloków dla cyklicznego problemu przepływowego z przezbrojeniami

PODSTAWY INFORMATYKI 1 PRACOWNIA NR 6

Podstawy Programowania

Funkcje wymierne. Funkcja homograficzna. Równania i nierówności wymierne.

Matematyczne Podstawy Informatyki

while(wyrażenie) instrukcja

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Rekurencja. Przykład. Rozważmy ciąg

Teoretyczne podstawy informatyki

Nierówność Krafta-McMillana, Kodowanie Huffmana

Algorytmy i Struktury Danych

Spacery losowe generowanie realizacji procesu losowego

Programowanie dynamiczne

Tadeusz Pankowski

Ćwiczenie 1. Wprowadzenie do programu Octave

Wstęp do programowania

7. Pętle for. Przykłady

Rekurencja. Dla rozwiązania danego problemu, algorytm wywołuje sam siebie przy rozwiązywaniu podobnych podproblemów. Przykład: silnia: n! = n(n-1)!

EGZAMIN - Wersja A. ALGORYTMY I STRUKTURY DANYCH Lisek89 opracowanie kartki od Pani dr E. Koszelew

PHP: bloki kodu, tablice, obiekty i formularze

Zmienne i struktury dynamiczne

Pętla for. Matematyka dla ciekawych świata -19- Scilab. for i=1:10... end. for k=4:-1:1... end. k=3 k=4. k=1. k=2

Wymagania edukacyjne z informatyki w klasie VIII

Tablice cz. I Tablice jednowymiarowe, proste operacje na tablicach

Transkrypt:

Michał Kazimierz Kowalczyk rok 1, semestr 2 nr albumu indeksu: 28969 Algorytmy i struktury danych Problem połączeń

Określenie problemu Problem połączeń możemy odnaleźć w wielu dziedzinach. Dla potrzeb tego projektu zobrazujemy go na przykładzie sieci informatycznych. Każde urządzenie istniejące w sieci posiada swój identyfikator, który umożliwia nam odwołanie się doń z zewnątrz. Jednak oprócz kwestii identyfikacji elementów w sieci, musimy rozważyć inny problem sposób łączenia się z omawianymi urządzeniami, a konkretniej wyznaczenie drogi, poprzez jaką muszą popłynąć dane od adresata dostały się do odbiorcy. Istnieją dwie możliwości: 1. Stworzenie połączenia bezpośrednio między każdym elementem (metoda inwazyjna, ale najprostsza). 2. Wykorzystanie istniejących połączeń pośrednich między urządzeniami (metoda nieinwazyjna, ale skomplikowana). Zakładamy więc, że możliwość pierwsza jest tylko ostatecznością i próbujemy ją omijać dzięki możliwości drugiej. Po tym krótkim wstępie spróbujemy określić nasz problem w sposób ogólny. Posiadając n elementów wyznaczyć m połączeń między nimi, ograniczając nawiązywanie ilość nowych połączeń poprzez wykorzystywanie ścieżek pośrednich. Ustalmy nazewnictwo, które pozwoli nam w prosty sposób określać bardziej abstrakcyjne podproblemy. Elementem będziemy nazywali każdy obiekt, który będziemy chcieli połączyć, natomiast identyfikatorem numer pozwalający nam się do niego odwołać. Ścieżką nazywamy zbiór połączeń między elementami tworzącymi sieć pozwalającą połączyć dowolne elementy zawierające się w niej. Stworzenie nowego połączenia nazwiemy scaleniem. Proponowane rozwiązanie Stwórzmy algorytm opierający się na drzewiastej strukturze danych, zaimplementujmy go w językach C/C++ i sprawdźmy jego działanie na różnych danych. Poprzez modyfikacje spróbujmy odnaleźć najbardziej optymalne rozwiązanie dla naszych potrzeb. Dla wybierania dwóch elementów ze zbioru do połączenia skorzystamy z funkcji losowych zawartych w GNU Scientific Library. Wszelki kod zawarty w tym projekcie został przetestowany w środowisku programistycznym Dev-C++ uruchomionym pod Windows Vista. Zacznijmy od najprostszego rozwiązania algorytmu szybkie wyszukiwanie.

Szybkie wyszukiwanie Algorytm ten będzie przeszukiwał tablicę identyfikatorów elementów z zakresu (n - 1) maksymalna ilość scaleń (twierdzenie z teorii grafów). Istniejące połączenie między elementami będziemy określać przez jeden wybrany identyfikator obiektu z tej sieci. Podczas łączenia dwóch sieci przepiszemy główny identyfikator pierwszej z nich do drugiej. Oto kod naszego algorytmu: /*Funkcja otrzymuje następujące argumenty: tablicę elementów, ich liczbę (n), ilość scaleń, wskaźnik na obiekt generatora liczb losowych*/ void quicksearch (unsigned array[], unsigned length, unsigned max, gsl_rng *r){ unsigned i, element1, element2, commonvalue; /*Dopóki ilość pozostałych scaleń jest różna od */ while (max){ /*Wygenerowanie losowych identyfikatorów elementów*/ element1 = gsl_rng_uniform (r) * length; element2 = gsl_rng_uniform (r) * length; /*Jeśli dwa identyfikatory wskazują na elementy z tej samej sieci, pomiń ten przypadek*/ /*Połącz dwie sieci*/ if (array[element1] == array[element2]) continue; commonvalue = array[element1]; for (i = ; i < length; i++) max--; if (array[i] == commonvalue) array[i] = array[element2];

Algorytm ten pozwolił nam utworzyć wykres zależności czasu liczonego w sekundach (oś Y) względem ilości elementów (oś X). 45 4 35 3 25 2 15 1 5 5 15 25 35 45 55 65 75 85 95 1% 75% 5% 25% Na wykresie znajdują się 4 krzywe, 25 / 5 / 75 / 1 %. Wartości te oznaczają ile z możliwych scaleń miało zostać dokonanych, gdzie 1 % to (n - 1) scaleń. Jak widzimy, zależność ta jest wykładnicza a ilość scaleń zmienia wpływa na funkcje proporcjonalnie względem czasu. Jak widzimy, algorytm ten nie nadaje do bardziej rozległych sieci. Spróbujmy go zmienić.

Szybkie scalanie Algorytm ten będzie również pobierał 2 losowe identyfikatory elementów. Jednak zamiast tworzyć wydzielone sieci, będzie tworzył ścieżki. Obiekty łączone będą swoimi korzeniami, scalając dwie gałęzie. Dwie sieci będą mogły zostać połączone tylko wtedy, gdy ich korzenie będą różne. /*Funkcja otrzymuje następujące argumenty: tablicę elementów, ich liczbę (n), ilość scaleń, wskaźnik na obiekt generatora liczb losowych*/ void quickmerge (unsigned array[], unsigned length, unsigned max, gsl_rng *r){ unsigned i, element1, element2; /*Dopóki ilość pozostałych scaleń jest różna od */ while (max){ /*Wygenerowanie losowych identyfikatorów elementów*/ element1 = gsl_rng_uniform (r) * length; element2 = gsl_rng_uniform (r) * length; /*Wyszukiwanie korzenia dla elementu 1*/ while (element1!= array[element1]) element1 = array[element1]; /*Wyszukiwanie korzenia dla elementu 2*/ while (element2!= array[element2]) element2 = array[element2]; /*Jeśli dwa identyfikatory wskazują na elementy z tej samej sieci, pomiń ten przypadek*/ if (element1 == element2) continue; /*Dołącz korzeń drugiej sieci za korzeń sieci pierwszej*/ array[element2] = element1; max--;

Wykres: 12 1 8 6 4 2 5 15 25 35 45 55 65 75 85 95 1% 75% 5% 25% Algorytm ten przy m = n - 1 działa znacznie wolniej, niż algorytm pierwszy. Za to przy już niewiele mniejszych ilościach scaleń, algorytm zdaje się być wiele bardziej wydajny. Zauważmy również, że krzywe straciły swoje własności (postać wykładniczą, proporcje odległości itd.). Jego spowolnienie względem szybkiego wyszukiwania prawdopodobnie spowodowany jest budowaniem bardzo skomplikowanych struktur drzewiastych. Spróbujemy je uprościć. Zrównoważone szybkie scalanie Aby zmniejszyć rozmiar drzew, możemy je inteligentniej budować. Dotychczasowo połączenie dwóch gałęzi polegało na połączeniu jednego z drugim, bez względu na ich rozmiary. Teraz spróbujemy to trochę zróżnicować: /*Funkcja otrzymuje następujące argumenty: tablicę elementów, tablicę rozmiarów gałęzi dla danego korzenia (początkowo każda gałąź ma rozmiar: 1), ich liczbę (n), ilość scaleń, wskaźnik na obiekt generatora liczb losowych*/ void balancedquickmerge (unsigned array[], unsigned offspring[], unsigned length, unsigned max, gsl_rng *r){ unsigned i, element1, element2;

/*Dopóki ilość pozostałych scaleń jest różna od */ while (max){ /*Wygenerowanie losowych identyfikatorów elementów*/ element1 = gsl_rng_uniform (r) * length; element2 = gsl_rng_uniform (r) * length; /*Wyszukiwanie korzenia dla elementu 1*/ while (element1!= array[element1]) element1 = array[element1]; /*Wyszukiwanie korzenia dla elementu 2*/ while (element2!= array[element2]) element2 = array[element2]; /*Jeśli dwa identyfikatory wskazują na elementy z tej samej sieci, pomiń ten przypadek*/ if (element1 == element2) continue; /*Jeśli pierwsza gałąź jest większa, dołącz do niej elementy gałęzi drugiej i vice-versa*/ if (offspring[element1] > offspring[element2]){ array[element2] = element1; offspring[element1]++; else{ array[element1] = element2; offspring[element2]++; max--;

Wykres: 5 4,5 4 3,5 3 2,5 2 1,5 1,5 5 15 25 35 45 55 65 75 85 95 1% 75% 5% 25% Jak łatwo zauważyć, algorytm ten działa znacznie lepiej. Widzimy, że przy wiele większym zakresie (nie jak wcześniej 5 1 ): 5 1 elementów otrzymujemy czasy zacznie mniejsze, niż przy poprzednich algorytmach. Jednak mimo wszystko dla bardzo dużych rozwiązań algorytm ten jest nadal zbyt wolny. Zrównoważone szybkie scalanie z kompresją ścieżek przez połowienie Naszym celem jest dalsza optymalizacja algorytmu, szczególnie pod względem rozbudowania struktury drzewiastej. Od teraz zastosujemy dla każdego elementu podczas kontaktu z nim przesunięcie go o 2 miejsca w stronę drzewa. W ten sposób do jednego korzenia dochodzić będzie większa ilość gałęzi. /*Funkcja otrzymuje następujące argumenty: tablicę elementów, tablicę rozmiarów gałęzi dla danego korzenia (początkowo każda gałąź ma rozmiar: 1), ich liczbę (n), ilość scaleń, wskaźnik na obiekt generatora liczb losowych*/ void compressionbyhalving (unsigned array[], unsigned offspring[], unsigned length, unsigned max, gsl_rng *r){ unsigned i, element1, element2, n1, n2;

while (max){ /*Wygenerowanie losowych identyfikatorów elementów*/ element1 = gsl_rng_uniform (r) * length; element2 = gsl_rng_uniform (r) * length; /*Wyszukiwanie korzenia dla elementu 1 wykorzystując metodę połowienia*/ while (element1!= array[element1]){ element1 = array[element1]; array[element1] = array[array[element1]]; /*Wyszukiwanie korzenia dla elementu 2 wykorzystując metodę połowienia*/ while (element2!= array[element2]){ element2 = array[element2]; array[element2] = array[array[element2]]; /*Jeśli dwa identyfikatory wskazują na elementy z tej samej sieci, pomiń ten przypadek*/ if (element1 == element2) continue; /*Jeśli pierwsza gałąź jest większa, dołącz do niej elementy gałęzi drugiej i vice-versa*/ if (offspring[element1] > offspring[element2]){ array[element2] = element1; offspring[element1]++; else{ array[element1] = element2; offspring[element2]++; max--;

Wykres: 4 3,5 3 2,5 2 1,5 1,5 5 15 25 35 45 55 65 75 85 95 1% 75% 5% 25% Jak widzimy, czas działania dla tego samego zakresu widocznie się zmniejszył. Jednak analizując strukturę drzewiastą łatwo zauważyć, że im drzewa posiadają krótsze gałęzie, tym algorytm działa szybciej. Spróbujmy więc skrócić jak najbardziej to tylko możliwe długość gałęzi. Zrównoważone szybkie scalanie z pełną kompresją ścieżek W momencie dostępu do elementu niebędącego korzeniem drzewa, możemy w pętli poprzyklejać każdy element do tego miejsca (korzenia). W ten sposób w zależności od wylosowanej liczby, możemy za jednym razem zredukować długość gałęzi do minimum. /*Funkcja otrzymuje następujące argumenty: tablicę elementów, tablicę rozmiarów gałęzi dla danego korzenia (początkowo każda gałąź ma rozmiar: 1), ich liczbę (n), ilość scaleń, wskaźnik na obiekt generatora liczb losowych*/ void fullcompression (unsigned array[], unsigned offspring[], unsigned length, unsigned max, gsl_rng *r){ unsigned i, element1, element2, container1, container2, buffer; while (max){

/*Wygenerowanie losowych identyfikatorów elementów*/ container1 = element1 = gsl_rng_uniform (r) * length; container2 = element2 = gsl_rng_uniform (r) * length; /*Wyszukiwanie korzenia dla elementu 1*/ while (element1!= array[element1]) element1 = array[element1]; /*Wyszukiwanie korzenia dla elementu 2*/ while (element2!= array[element2]) element2 = array[element2]; /*Przyklejanie elementów gałęzi tworzonej z elementu 1 do korzenia*/ while (array[container1]!= element1){ buffer = container1; container1 = array[container1]; array[buffer] = element1; offspring[element1]++; /*Przyklejanie elementów gałęzi tworzonej z elementu 2 do korzenia*/ while (array[container2]!= element2){ buffer = container2; container2 = array[container2]; array[buffer] = element2; offspring[element2]++; /*Jeśli dwa identyfikatory wskazują na elementy z tej samej sieci, pomiń ten przypadek*/ if (element1 == element2) continue; /*Jeśli pierwsza gałąź jest większa, dołącz do niej elementy gałęzi drugiej i vice-versa*/ if (offspring[element1] > offspring[element2]){ array[element2] = element1;

offspring[element1]++; else{ array[element1] = element2; offspring[element2]++; max--; Wykres: 3,5 3 2,5 2 1,5 1,5 5 15 25 35 45 55 65 75 85 95 1% 75% 5% 25% Jak widzimy, udało nam się wprowadzić kolejne optymalizacje. Jeśli jednak wyniki nadal nie są dla nas satysfakcjonujące, można wzbogacić obecny algorytm o możliwość szybszego odnajdywania korzenia, czym jednak nie będziemy się już zajmować w tym artykule.

Porównania algorytmów Wykres przedstawia zależność czasu i ilości elementów przy m = n - 1 5 4,5 4 3,5 3 2,5 2 1,5 1,5 5 15 25 35 45 55 65 75 85 95 zrównoważone szybkie scalanie pełna kompresja ścieżek kompresja przez połowienie Jak widzimy na powyższym wykresie wszystkie złożone algorytmy wywodzące się z szybkiego scalania dla mniejszych liczb działają identycznie. Dopiero przy 25 elementów widać ich rozwarstwienie.,6,5,4,3,2,1 5 15 25 35 45 55 65 75 85 95 zrównoważone szybkie scalanie pełna kompresja ścieżek kompresja przez połowienie Na tym wykresie widzimy wyniki szybkich algorytmów dla m = ¾ (n - 1). Widzimy, że różnice między wynikami algorytmów zaczynają się zacierać, a ich zależność przybiera postać liniową.

,16,14,12,1,8,6,4,2 5 15 25 35 45 55 65 75 85 95 szybkie scalanie kompresja przez połowienie zrównoważone szybkie scalanie pełna kompresja ścieżek Natomiast powyższy wykres przedstawia algorytmy przy m = ¼ (n 1). Jak łatwo zauważyć, tworząc go wzięliśmy pod uwagę często odrzucany algorytm szybkiego scalania. Podsumowanie Analizując rozwój algorytmów pozwalających nam rozwiązać problem połączeń, możemy wywnioskować, że: 1. Im algorytm jest bardziej złożony poprzez zastosowanie odpowiedniego poziomu abstrakcji z odpowiednimi strukturami danych, przy dużych wymaganiach (takich jak duża ilość, czy też szczegółowość obliczeń) otrzymujemy programy działające o wiele szybciej, niż by się wydawało stosując prosty algorytm, zawierający teoretycznie mniejszą liczbę instrukcji. 2. Im mniejsze wymagania wobec algorytmu, względem szczegółowości wyników, czy też ilości danych do przetworzenia, tym bardziej opłacalne jest stosowanie prostych algorytmów. Literatura Robert Sedgewick, Algorytmy w C++, ReadMe, Warszawa, 1999