Tomasz Czekala
Kamil 艢lesi艅ski
Izomorfizm graf贸w acyklicznych skierowanych z kolorowymi wierzcho艂kami
I. Przedstawienie problemu i opis jego rozwi膮zania
Problem polega na stworzeniu efektywnego algorytmu stwierdzaj膮cego czy dwa grafy acykliczne skierowane z kolorowymi wierzcho艂kami s膮 izomorficzne.
Problem rozstrzygania izomorficzno艣ci dw贸ch graf贸w nale偶y do klasy NP, ale prawdopodobnie nie jest problemem NP-zupe艂nym. Z drugiej strony, nie s膮 znane wielomianowe algorytmy deterministyczne, probabilistyczne, ani kwantowe rozwi膮zuj臋ce ten problem. Niemniej jednak, dla wielu specjalnych klas graf贸w izomorfizm mo偶e zosta膰 rozstrzygni臋ty w czasie wielomianowym. Dla naszej klasy problem贸w starali艣my si臋 skonstruowa膰 algorytm dzia艂aj膮cy jak najszybciej, sk艂adaj膮cy si臋 z dw贸ch cz臋艣ci: jednej dzia艂aj膮cej w czasie wielomianowym, a drugiej w czasie wyk艂adniczym. Starali艣my si臋 tak zoptymalizowa膰 cz臋艣膰 wielomianow膮, aby cz臋艣膰 dzia艂aj膮ca w czasie wyk艂adniczym mia艂a jak najmniej danych do przetworzenia.
Algorytm wygl膮da nast臋puj膮co:
1) Wczytujemy dwa grafy do sprawdzenia. „W” - wej艣ciowy i „R”- referencyjny.
2) Dokonujemy wst臋pnego, szybkiego, liniowego sprawdzenia czy grafy mog膮 by膰 izomorficzne. Sprawdzamy liczno艣膰 wierzcho艂k贸w, kraw臋dzi i kolor贸w.
3) Z graf贸w W i R tworzymy grafy o przeciwnie skierowanych kraw臋dziach, odpowiednio OW - odwr贸cony wej艣ciowy i OR - odwr贸cony referencyjny.
4) Dzielimy wszystkie grafy na warstwy w nast臋puj膮cy spos贸b:
a) W warstwie pierwszej znajduj臋 si臋 wierzcho艂ki do kt贸rych nie wchodz膮 kraw臋dzie.
b) W warstwie n-tej znajduj膮 si臋 wierzcho艂ki, kt贸re nie znajduj膮 si臋 w poprzednich warstwach, do kt贸rych dochodz膮 kraw臋dzie z warstwy n-1.
5) Dokonujemy por贸wnania warstw oryginalnych i odwr贸conych. W implementacji nie tworzymy oddzielnych graf贸w, tylko ka偶dy wierzcho艂ek ma numer warstwy oryginalnego i odwr贸conego grafu, ale dla jasno艣ci w dalszej cz臋艣ci dokumentacji b臋dziemy nazywa膰 to graf oryginalny W i odwr贸cony OW. Sprawdzamy takie rzeczy jak ilo艣膰, kolory i stopnie wierzcho艂k贸w w odpowiednich warstwach graf贸w oryginalnych; jednocze艣nie dla ka偶dego wierzcho艂ka z graf贸w W i OW tworzymy list臋 wierzcho艂k贸w z graf贸w R i OR, kt贸re mog膮 im odpowiada膰; na tym etapie algorytmu istnieje du偶e prawdopodobie艅stwo odrzucenia izomorfizmu graf贸w lub znalezienia jednoznacznie okre艣lonych wierzcho艂k贸w.
Przyk艂ad, gdzie podzia艂 na warstwy (i odwr贸cenie kraw臋dzi!) jest przydatne zosta艂 przedstawiony na poprzednim diagramie w punkcie 4.b. Wida膰, i偶 na grafach odwr贸conych pojawi艂a si臋 warstwa z tylko jednym wierzcho艂kiem.
6) Dla ka偶dego wierzcho艂ka z W i OW bierzemy cz臋艣膰 wsp贸ln膮 list mo偶liwych, odpowiadaj膮cych im wierzcho艂k贸w z R i OR (dzi臋ki nie tworzeniu w programie grafu odwr贸conego, ten iloczyn jest wykonywany automatycznie przy por贸wnywaniu wierzcho艂k贸w). Dzi臋ki temu, prawdopodobnie bardzo, zaw臋偶amy ilo艣膰 kombinacji do sprawdzenia. Istnieje r贸wnie偶 szansa, 偶e na tym etapie dostaniemy kolejne, jednoznacznie zidentyfikowane, odpowiadaj膮ce sobie wierzcho艂ki z W i R.
7) Na tym etapie, ka偶dy wierzcho艂ek z graf贸w W i OW ma przypisan膮 list臋 mo偶liwych wierzcho艂k贸w z R i OR. Istnieje r贸wnie偶 szansa, 偶e niekt贸re wierzcho艂ki b臋d膮 zidentyfikowane jednoznacznie. Na podstawie jednoznacznie wyznaczonych wierzcho艂k贸w staramy si臋 wyznaczy膰 jednoznacznie kolejne wierzcho艂ki. Jest to proces iteracyjny. Gdy nie jeste艣my w stanie wyznaczy膰 wi臋cej wierzcho艂k贸w to przechodzimy do kolejnego etapu.
8) Na zbiorze wierzcho艂k贸w kt贸re nie zosta艂y wyznaczone jednoznacznie (poprzednie kroki algorytmu stara艂y si臋 zmniejszy膰 liczno艣膰 tego zbioru) wykonujemy proste sprawdzanie wszystkich mo偶liwych kombinacji. Krok ten ma z艂o偶ono艣膰 wyk艂adnicz膮.
Na tym etapie algorytmu, wszystkie niejednoznaczne wierzcho艂ki s膮 podzielone na zbiory. W ka偶dym zbiorze znajduj膮 si臋 wierzcho艂ki z grafu W i R kt贸re mog膮 by膰 ze sob膮 izomorficzne. Wierzcho艂ki z r贸偶nych zbior贸w na pewno nie s膮 ze sob膮 izomorficzne. Dla tych zbior贸w generujemy iteracyjnie wszystkie mo偶liwe przypisania niejednoznacznych wierzcho艂k贸w grafu W do niejednoznacznych wierzcho艂k贸w grafu R. Samo podzielenie wierzcho艂k贸w na takie zbiory daje bardzo wymiern膮 korzy艣膰. Przyk艂adowo: dla grafu o 12 wierzcho艂kach bez podzia艂u na takie zbiory istnieje 12! kombinacji - 479001600. Gdyby uda艂o si臋 podzieli膰 wierzcho艂ki chocia偶 na 3 zbiory po 4 elementy to mamy tylko (4!)^3 = 13824 kombinacji do sprawdzenia.
9) Je艣li nie uda艂o si臋 wyznaczy膰 jednoznacznie wszystkich wierzcho艂k贸w, lub nie istnieje kombinacja wierzcho艂k贸w kt贸ra spe艂nia za艂o偶enia izomorficzno艣ci W i R to grafy nie s膮 izomorficzne.
II. Analiza poprawno艣ci i z艂o偶ono艣ci algorytmu
Ca艂y proces sk艂ada si臋 z kilku niezale偶nych krok贸w, wi臋c trzeba je po kolei opisa膰.
Wczytanie - algorytm oczywisty, z艂o偶ono艣膰 O(n + m)
Wst臋pne sprawdzenie podstawowych danych statystycznych (ilo艣膰 wierzcho艂k贸w danego koloru, itp.) - wymaga jedynie zliczenia odpowiednich parametr贸w, z艂o偶ono艣膰 O(n + m)
Tworzenie grafu odwrotnego - kopia wierzcho艂k贸w i kraw臋dzi, tylko odwracamy kraw臋dzie. Z艂o偶ono艣膰 O(n + m). My jednak tego nie robimy.
Dzielenie na warstwy - Przeszukiwanie wszerz maj膮c na pocz膮tku kolejki wszystkie wierzcho艂ki, do kt贸rych nie wchodz膮 kraw臋dzie. Z艂o偶ono艣膰 O(n + m).
Por贸wnanie warstw - Sprawdzamy ka偶dy z ka偶dym. Z艂o偶ono艣膰 O(k^2), gdzie k jest ilo艣ci膮 wierzcho艂k贸w w warstwie. Budujemy zbiory wierzcho艂k贸w niejednoznacznych.
Por贸wnanie grafu wej艣ciowego i odwr贸conego. Dzi臋ki odpowiednio dobranej strukturze nie robi si臋 to automatycznie.
Szukanie jednoznacznych wierzcho艂k贸w na podstawie jednoznacznych. Ze wzgl臋du na to, jakie grafy b臋dziemy dostawa膰 w programie u偶ytkowym, zdecydowali艣my si臋 zrobi膰 tylko sprawdzanie, czy wierzcho艂ek niejednoznaczny na jednoznacznego rodzica/dziecko, kt贸ry ma tylko 1 dziecko/rodzica danego stopnia. W praktyce w grafach kt贸re b臋dziemy sprawdza膰 dochodz膮c do tego etapu b臋dzie bardzo ma艂o wierzcho艂k贸w niejednoznacznych i zapewne b臋d膮 one mog艂y by膰 wyeliminowane w podany spos贸b, chocia偶by dlatego, 偶e kolory rzadko si臋 b臋d膮 powtarza膰.
Sprawdzanie wszystkich pozosta艂ych mo偶liwo艣ci. Z艂o偶ono艣膰 O(k! ^ (n / k)).
III. Opis programu
Algorytm zosta艂 zaimplementowany z my艣l膮 o integracji z wi臋ksz膮 aplikacj膮, a zatem jest w formie kodu 藕r贸d艂owego. Podstawowa funkcjonalno艣膰 to funkcja assessIsomorphismBetweenGraphs klasy GraphUtilities, kt贸ra sprawdza czy dane grafy s膮 izomorficzne. Dodatkowo, nasza biblioteka udost臋pnia funkcj臋 hasError kt贸ra mo偶e s艂u偶y膰 do sprawdzania czy graf zawiera cykl, kraw臋dzie wielokrotne oraz czy jego kraw臋dzie s膮 poprawnie zdefiniowane.
Ze wzgl臋du na swoj膮 charakterystyk臋 nasza biblioteka nie posiada UI. Jej dzia艂anie mo偶na obejrze膰 i przetestowa膰 za pomoc膮 do艂膮czonych (b膮d藕 w艂asnych) test贸w jednostkowych. Za艂膮czone testy zosta艂y opisane w rozdziale V.
IV. Opis techniczny programu
Wszelkie z艂o偶one operacje zosta艂y opatrzone stosownymi komentarzami w kodzie.
V. Opis test贸w
Testowanie swojego algorytmu oparli艣my g艂贸wnie na testach jednostkowych (JUnit). Testowane by艂y zar贸wno algorytmy pomocnicze (np. iteracyjne generowanie permutacji) jak i algorytmy kt贸re same stworzyli艣my. W miar臋 mo偶liwo艣ci tworzone by艂y testy automatyczne, np. automatyczne permutowanie grafu i sprawdzanie czy funkcja sprawdzaj膮ca izomorficzno艣膰 zwraca poprawny wynik, niemniej jednak, niekt贸re testy by艂y preparowane r臋cznie w celu sprawdzenia konkretnych w艂a艣ciwo艣ci algorytmu. Stworzone by艂y r贸wnie偶 testy wydajno艣ciowe.
Poni偶ej zamieszczona zosta艂a lista klas wraz z opisami test贸w kt贸re zawieraj膮.
1. PermutationsTest
a) test() - testuje klas臋 PossibilitySet oraz funkcj臋 s艂u偶膮c膮 do generowania permutacji zbioru 8-elementowego; wynik dzia艂ania wypisywany na konsol臋
2. GraphNodePermutationsTest
a) graphNodePermutations() - testuje funkcj臋 PermuteGraphNodes klasy PermutationsGenerator; wynik dzia艂ania wypisywane na konsol臋
3. SimpleTest
a) allPermutations() - testujemy algorytm na grafach, kt贸re wymagaj膮 sprawdzania wszystkich kombinacji
b) cycleTest1() - sprawdzamy czy dane grafy maj膮 cykle; jest to test funkcji dodatkowej, nie sprawdza izomorficzno艣ci
c) efficiencyTest1() - 100'000 razy sprawdzamy izomorficzno艣膰 takich graf贸w, jak w punkcie g); zajmuje to oko艂o jednej sekundy
d) efficiencyTest2() - budujemy 2 d艂ugie grafy (podw贸jne listy maj膮ce 艂膮cznie 100 wierzcho艂k贸w) i 10'000 razy sprawdzamy izomorfizm; wykonuje si臋 kilka sekund
e) emptyGraphsTest() - testujemy przypadki skrajne, np. obs艂ug臋 grafu pustego
f) findClearFromClear() - budujemy graf, kt贸ry sk艂ada si臋 z dw贸ch roz艂膮cznych ci膮g贸w wierzcho艂k贸w, przy czym 1 wierzcho艂ek jest innego koloru. Na jego podstawie mo偶na wyznaczy膰 wszystkie pozosta艂e wierzcho艂ki jako jednoznaczne. Sprawdzamy, czy tak si臋 dzieje
g) isomorphismTest1() - tworzymy 2 grafy 10 elementowe, sprawdzamy ich izomorficzno艣膰 z samym sob膮, po czym ze sob膮 nawzajem. Grafy wygl膮daj膮 nast臋puj膮co:
tylko maj膮 inn膮 numeracj臋 wierzcho艂k贸w. S膮 te偶 robione zmiany niekt贸rych kraw臋dzi w jednym z nich i jest sprawdzane, czy grafy nie s膮 izomorficzne.
h) test1() - sprawdzamy, czy konstruktor naszego grafu rzuci wyj膮tek IllegalArgumentException je艣li dostanie niepoprawne wej艣cie
i) test2() - sprawdzamy, czy je艣li w danej warstwie (jest tylko jedna) mamy inn膮 ilo艣膰 wierzcho艂k贸w danego typu (pomi臋dzy grafem W i R), to czy grafy zostan膮 uznane za nie izomorficzne
4. AdvancedTests
a) bigGraph() - testuje graf automatycznie tworz膮c jego permutacje, nacisk po艂o偶ony na znajdowanie wierzcho艂k贸w na podstawie warstw oraz na znajdowanie jednoznacznych wierzcho艂k贸w na podstawie ju偶 znalezionych jednoznacznych wierzcho艂k贸w
b) longGraph() - testuje permutacje grafu
, nacisk po艂o偶ony na por贸wnywanie warstw
c) test1() - testuje grafy, nacisk po艂o偶ony na sprawdzanie wszystkich mo偶liwych kombinacji
OW
W
R
OR