Pamiętacie z dzieciństwa zabawę w rysowanie koperty bez odrywania długopisu? To jest właśnie problem znajdowania łańcucha Eulera w grafie. Gdyby dodać warunek, że musimy zacząć i skończyć rysować w tym samym miejscu (tak się nie da) to byłby to problem znajdowania cyklu Eulera.
Cykl Eulera, to taki cykl w grafie, która zawiera każdą krawędź
grafu, przy czym każdą dokładnie raz. Natomiast łańcuch Eulera to taka droga
(czyli nie musi się kończyć tam gdzie zaczyna), która zawiera każdą krawędź
grafu dokładnie raz.
Dotyczy to zarówno grafów nieskierowanych, jak i
skierowanych.
Żeby znaleźć cykl/łańcuch Eulera w grafie trzeba najpierw
sprawdzić czy istnieje. Przede wszystkim (i nie można o tym zapomnieć!) należy
sprawdzić czy graf jest spójny (ale nie liczymy samotnych wierzchołków). Dla
grafu skierowanego należy sprawdzić spójność jego nieskierowanego odpowiednika
(tzn. chwilowo pozbawiamy krawędzi kierunku). Oczywiście gdy graf nie jest
spójny to nie istnieje cykl Eulera (no bo jak obejść wszystkie krawędzie jeżeli
nie można do którejś dojść?).
Teraz wystarczy zliczyć ile jest krawędzi
wychodzących/wchodzących z/do każdego wierzchołka i sprawdzić jeden prosty
warunek. W przypadku grafów skierowanych cykl Eulera istnieje wtedy i tylko
wtedy gdy do każdego wierzchołka wchodzi tyle samo krawędzi co wychodzi (uwaga:
pętla jednocześnie wchodzi i wychodzi). Natomiast w przypadku grafów
nieskierowanych z każdego wierzchołka musi wychodzić parzysta liczba krawędzi
(jedna połowa okaże się wchodzącymi, a druga połowa wychodzącymi).
Jeżeli
chcemy znaleźć tylko łańcuch Eulera to możemy sobie pozwolić na drobne
odstępstwo od powyższego warunku w dokładnie dwóch wierzchołkach. Czyli w
przypadku grafów nieskierowanych dwa wierzchołki mogą mieć nieparzystą liczbę
sąsiadów; w przypadku grafów skierowanych z dwóch wierzchołków liczba krawędzi
wchodzących i wychodzących może się różnić o jeden.
Sprawdziwszy czy cykl istnieje możemy się zabrać za jego
znalezienie. Zatem znajdujemy dowolny cykl w grafie (oznaczmy go sobie przez
C0), startując z dowolnego wierzchołka. Następnie usuwamy wszystkie
krawędzie tego cyklu. Prawdopodobnie spowoduje nam to rozspójnienie grafu i
pojawi się kilka spójnych składowych (jeśli się nie pojawią to znaczy, że już
wszystkie krawędzie zostały użyte i cykl znaleźliśmy). Każda z tych składowych
ma punkt (co najmniej jeden) wspólny z naszym cyklem i właśnie od tego punktu
zaczynamy rekurencyjne szukanie cyklu Eulera w tej składowej (i tak dla każdej
składowej).
Następnie należy znaleziony cykl w każdej składowej połączyć z
głównym cyklem. Jest to rzecz dość naturalna: jeżeli przeglądając cykl C0
dojdziemy do wierzchołka, który należy do jakiejś składowej, to zamiast podążać
dalej tym cyklem wchodzimy w cykl tej składowej i dopiero po powrocie z niego
kontynuujemy podróż w C0.
Może dla pełnej jasności ujmę to nieco inaczej (mniej
książkowo). I to raczej tej wersji będę trzymać się w podanym poniżej
algorytmie.
Znajdujemy jakiś cykl w grafie. Następnie przechodzimy po
wierzchołkach, jednocześnie zapisując przebyte krawędzie na jakąś ogólną listę.
Jeżeli dojdziemy do wierzchołka, z którego wychodzi krawędź, której jeszcze nie
przeszliśmy to z tego wierzchołka zaczynamy rekurencyjne znajdowanie (pod)cyklu
Eulera (a wynik tego powinien także znaleźć sie na tej ogólnej liście). Trzeba
jednak pamiętać, żeby nie przechodzić krawędzi, które już raz przeszliśmy
(kiedykolwiek). Po wyjściu z rekurencji można kontynuować podróż po głównym
cyklu, chyba że z tego wierzchołka jeszcze wystają nieprzebyte krawędzie.
W
praktyce znalezione cykle będę trzymać na stosie.
Jeżeli chcemy znaleźć jedynie łańcuch Eulera a nie cykl, to musimy wystartować z jednego wierzchołka, w którym nie był spełniony powyższy warunek. Później musimy znaleźć jakąkolwiek drogę do drguiego takiego wierzchołka. Dalej już jest tak samo - przeglądamy tą drogę i rekurencyjnie wyszukujemy cykle Eulera.
Najlepszą reprezentacją grafu do wykonania tego algorytmu są
listy następników. Jeżeli ktoś koniecznie potrzebuje koniecznie inną
reprezentację powinien zasymulować korzystanie z niej jak korzystanie z list
następników.
Taka reprezentacja daje nam tą przewagę, że jeżeli odwiedzimy
jakąś krawędź to przesuwamy wskaźnik na liście i więcej do tej krawędzi nie
wrócimy. Przez to nie trzeba ani usuwać krawędzi z grafu, ani zapamiętywać
przebyte krawędzie.
No to czas na (pseudo)kod. Założenie jest takie, że warunek
istnienia cyklu Eulera został sprawdzony i wypadł pozytywnie.
Żeby znaleźć
cykl Eulera należy wywołać procedurę EulerCycle z dowolnym wierzchołkiem
(ale nie samotnym) podanym jako parametr.
Wynikiem jej działania będzie
lista, w której będą zapisane kolejne wierzchołki (mogą się powtarzać) cyklu
Eulera, które już łatwo przerobić na krawędzie. Lista jest jednak zapisana w
odwrotnej kolejności, dlatego najlepiej jakby była strukturą LIFO - wtedy
kolejność zdejmowania z niej byłaby prawidłowa. Można ten problem jeszcze
załatwić np. używając zamiast stosu (patrz algorytm) kolejkę.
procedure EulerCycle(v: Integer); {znajduje cykl Eulera poczawszy od wierzch. v; nie przechodzi przebytych krawedzi} begin w:=v; Put_On_Stack(w); repeat {znajdowanie pewnego cyklu} w:=Get_Next_Succ(w); {pobiera kolejny wierzch. z listy nastepnikow w; przesuwa wskaznik na tej liscie na nastepny wierzch.} Put_On_Stack(w); {dodaje kolejny wierzcholek do stosu} until w=v; {konczy petle, gdy cykl sie zamknie} Get_Form_Stack(w); repeat {przegladanie znalezionego cyklu} if No_Next_Succ(w) then {z w nie wychodzi wiecej krawedzi} Put_On_List(w) {wstawia wierzcholek na ogolna liste} else EulerCycle(w); {znajdujemy rekurencyjnie podcykl Eulera startujac z w} Get_Form_Stack(w); {bierzemy i usuwamy kolejny wierzch. cyklu ze stosu} until w=v; Put_On_List(v); end;
Powyższy algorytm działa dla grafów skierowanych. Dla grafów
nieskierowanych powstaje jednak problem. W powyższym algorytmie bazowałem na
tym, że jak przesunąłem wskaźnik na liście następników, to rozważana krawędź już
się nigdy więcej nie pojawi. Niestety w grafach nieskierowanych ta sama krawędź
jest na dwóch listach. Dlatego trzeba dodatkowo pamiętać krawędzie
użyte.
Prosty kod pobrania kolejnego następnika (patrz pierwsza pętla) oraz
sprawdzenia istnienia następnika (patrz druga pętla) powinien być zastąpiony
przez mniej więcej taki:
{ pobranie kolejnego następnika } [...] repeat x:=Get_Next_Succ(w); {szuka następnika w} until Not_Used(x,w); {az znajdzie nieuzyta krawedz} Used(x,w); Used(w,x); {zapamietuje, ze krawedzie zostaly uzyte} w:=x; [...]
{ sprawdzenie istnienia kolejnego następnika } [...] No_Succ:=True; while not No_Next_Succ do begin x:=Watch_Next_Succ(w); {bierze nastepnika, ale nie przesuwa wskaznika} if Not_Used(x,w) then {jezeli nie uzyty to ...} begin NoSucc:=False; {stwierdza, ze istnieja nastepniki i opuszcza petle} Break; end; x:=Get_Next_Succ(w); {przesuwa wskaznik na liscie nastepnikow w} end; if NoSucc then [...]
Może powiecie, że się powtarzam i jestem nudny, ale muszę to napisać. Algorytm przegląda każdą krawędź pewną stałą (niewielką) liczbę razy. Zatem działa w czasie O(E) (o ile została wybrana sensowna reprezentacja grafu).