AAL - Ubrania
Transkrypt
AAL - Ubrania
Analiza Algorytmów AAL-3-LS ubrania Paweł Kubik Treść Posiadasz kolekcje n ubrań. O każdej parze dwóch ubrań potrafisz powiedzieć, czy pasują do siebie. Należy rozmieścić ubrania w jak największej liczbie szaf w taki sposób, aby wybierając po jednej dowolnej rzeczy z każdej szafy, mieć gwarancję, że pasują do siebie. Interpretacja Problem można sprowadzić do poziomu teorii grafów. Przyjmijmy, że ubrania to wierzchołki grafu, a relacja pasowania do siebie to krawędź pomiędzy wierzchołkami. Problem sprowadza się wówczas do znalezienia największej liczby takich rozłącznych zbiorów wierzchołków (dalej klastrów), że dowolni reprezentanci tych klastrów będą razem tworzyć graf pełny. Klastry te są odpowiednikami szaf. Alternatywnie można przyjąć, że krawędź między dwoma wierzchołkami istnieje wtedy, kiedy ubrania są w relacji niepasowania. Poprzednią interpretację można sprowadzić do tej postaci zastępując graf jego dopełnieniem. Problem sprowadza się w takim przypadku do znalezienia takich klastrow, aby ich dowolni reprezentanci nie mieli żadnej wspólnej krawędzi. Wynika to z analogii do poprzedniej postaci problemu, oraz bezpośrednio z treści zadania. Istnieją dwa rodzaje przypadków brzegowych (opisane dla I-szego podejścia): 1. graf pełny, gdzie każdy wierzchołek powinien być przypisany do oddzielnego klastra 2. graf o pustym zbiorze krawędzi, gdzie wszystkie wierzchołki muszą się znaleźć w jednym klastrze Wszystkie pozostałe przypadki znajdują się pomiędzy powyższymi z czego wynika, że zawsze istnieje rozwiązanie problemu. Rozwiązanie Dla ułatwienia przyjmijmy postać problemu w której krawędzie odpowiadają niepasowaniu pary ubrań. Jeśli para wierzchołków jest połączona krawędzią to koniecznie muszą się one znaleźć w tym samym klastrze. W przeciwnym wypadku należeliby do zbioru reprezentantów niespełniających kryteriów. Relacja przynależności do tego samego klastra jest przechodnia, co oznacza, że każda spójna składowa grafu musi zawierać się w jednym klastrze. Warunkiem wystarczającym do maksymalizacji liczby klastrów jest umieszczenie każdej spójnej składowej w oddzielnnym klastrze. Podsumowując problem można sprowadzić do znalezienia spójnych składowych grafu. Jeśli jednak założyć, że na wejściu podany jest graf, w którym krawędzie oznaczają pasowanie ubrań, to należy najpierw znaleźć jego dopełnienie. Implementacja Implementacja rozwiązuje wariant problemu w którym krawędzie oznaczają pasowanie do siebie ubrań. Graf jest reprezentowany jako listy sąsiedztwa. Dla uproszczenia każdemu wierzchołkowi przyporządkowywana jest unikalna liczba ze zbioru N ∪ {0}. Rozwiązanie składa się z dwóch kroków - szukania dopełnienia grafu, oraz szukania spójnych składowych dopełnienia. W przypadku par uporządkowanych monotonicznie kolekcji można uzyskać ich sumę, różnicę oraz przecięcie w czasie liniowym. Jeśli zatem listy sąsiedztwa są uporządkowane względem numerów wierzchołków, to istnieje możliwość znalezienia dopełniania grafu w czasie O(n2 ), poprzez działania różnicy na listach sąsiedztwa i uporzątkowanej kolekcji wszystkich wierzchołków. Faktyczny koszt tej operacji jest niezależny od gęstości grafu. Operacja może zostać wykonana in-place, a zatem ma stały koszt pamięciowy, w praktyce jednak funkcja z biblioteki <algorithm> wymaga dodatkowego bufora, co daje koszt liniowy. W obu przypadkach jest on zaniedbywalny z perspektywy całego problemu. Do odnalezienia spójnych składowych grafu użyto algorytmu przeszukiwania wszerz (BFS) o złożoności obliczeniowej i pamięciowej O(|V | + |E|). Złożoność tego kroku i tak jest ograniczona przez rozmiar danych wejściowych, więc nie ma sensu szukać lepszego rozwiązania. Koszt odnajdywania dopełnienia grafu jest dominujący, zatem całkowita asymptotyczna złożoność rozwiązania to O(n2 ). W pesymistycznym przypadku jest to również złożoność obsługi danych wejściowych, zatem można ją uznać za satysfakcjonującą. Kod Cały kod projektu znajduje się w podkatalogu src. Podkatalog src/test zawiera ręczne testy jednostkowe, które nie są linkowane z głównym plikiem wykonywalnym. W podkatalogu res znajdują się przykładowe pliki wejściowe *.in, oraz skrypty pomocne przy analizowaniu wyników. Pliki wykonywalne i pośrednie znajduja się w katalogu build. Projekt korzysta z systemu budowania premake4, który pozwala wygenerować pliki konfiguracyjne komplilacji dla różnych środowisk - m. in. GNU make. Aby zbudować projekt wygenerowany dla tego narzędzia należy wywołać polecenie make w katalogu build. Plik wykonywalny nazywa się ubrania. Klasy: * Graph - zawiera implementację grafu przy użyciu list sąsiedztwa. Przypisuje każdemu wierzchołkowi wartość liczbową i przechowuje mapowanie nazwa ↔ liczba. Zawiera metodę invert, która przekształca dany graf w jego dopełnienie. * Problem - odpowiada za reprezentację problemów i ich rozwiązań. Jeden z konstruktorów pozwala na wygenerowanie losowego problemu dla zadanej liczby wierzchołków i gęstości grafu. Drugi z konstruktorów pozwala na użycie parsera w celu odczytu problemu z pliku. Zawiera metodę solve, która generuje rozwiązanie wraz z czasami wykonania poprzez wywołanie Graph::invert oraz Problem:: clusterize . Wyniki i pomiary czasu mogą być potem odczytane, albo zapisane do pliku poprzez parser. Metoda clusterize odpowiada za odnalezienie spójnych składowych. * DotParser - pozawala na prosty odczyt i zapis grafu w formacie dot. Obsługiwana jest tylko część funkcjonalności tego języka zademonstrowana w plikach przykładowych. * Profiler - klasa pomocnicza pozwalająca na wykonywanie serii losowych testów. Skrypty pomocnicze: * drawall.sh - generuje plik .svg dla każdego pliku .out w aktualnym katalogu * match.py (skrypt python 3) - dla zadanego pliku powstałego powstałego w wyniku polecenia ”ubrania time ... ”, generuje ciąg współczynników zgodności oceny teoretycznej z pomiarem czasu i wypisuje go na standardowe wyjście. * plot .py (skrypt python 3) - jak match.py, ale zamiast wypisywać wartości tworzy wykres, który zapisuje w formacie SVG Pomiary czasu wykonania W celu sprawdzenia oszacowanej złożoności wykonano serię testów dla różnych gęstości grafów przy użyciu trybu wykonania z pomiarem czasu: . / u b r a n i a time 10 5000 0 . 2 r e s / t i m e s 2 0 . c s v . / u b r a n i a time 10 5000 0 . 5 r e s / t i m e s 5 0 . c s v . / u b r a n i a time 10 5000 0 . 8 r e s / t i m e s 8 0 . c s v Wykresy wygenerowane dla powyższych poleceń: Rysunek 1: Dla grafu o gęstości 20% Rysunek 2: Dla grafu o gęstości 50% Rysunek 3: Dla grafu o gęstości 80% Poza przewidywalnym błędem dla małych wartości liczby wierzchołków współczynnik zgodności jest zbliżony do 1. Możliwości ulepszenia rozwiązania Pesymistyczna złożoność rozwiązania jest ograniczona przez rozmiar danych, ale można poprawić złożoność optymistyczną, która obecnie jest taka sama. Operowanie na dopełnieniu grafu powoduje, że czas wykonania jest symetryczny ze względu na gęstość grafu - grafy gęste są wymagające ze względu na obsługę wejścia, a grafy rzadkie mają gęste dopełnienia. Jednym ze sposobów jest częściowe generowanie dopełnienia grafu w locie podczas szukania spójnych składowych, zamiast robienia tego w dwóch krokach. 1. Stwórz zbiór wszystkich wierzchołków W 2. Dla każdego wierzchołka v (a) Oblicz różnicę W oraz zbioru sąsiadów v (b) (wykonaj kroki oryginalnego algorytmu wykorzystując wyżej obliczony zbiór jako zbiór sąsiadów v) (c) Odejmij od W odwiedzone wierzchołki Metoda opiera się na spostrzeżeniu, że można zignorować krawędzie, które łączą wierzchołki przedzielone do tego samego klastra, ponieważ nie wnoszą one żadnej informacji. Dla każdego wierzchołka należy obliczyć różnicę zbioru pozostałych wierzchołków i jego sąsiadów o koszcie proporcjonalnym do licznośći większego z tych zbiorów, dlatego złożoność tej metody to O(|V | + |E|).