Ciągi de Bruijna generowanie, własności Jakub Radoszewski
Ciąg de Bruijna rzędu n cykliczne słowo zerojedynkowe długości 2n, w którym każde podsłowo długości n występuje dokładnie raz np. 1010000110010111 (rząd 4)
Zastosowania szybkie generowanie słów zerojedynkowych długości n – liczb od 0 do 2n-1 lub podzbiorów zbioru n-elementowego zastosowania w: elektronice, sieciach komputerowych, biologii obliczeniowej, kryptografii Ciekawe same w sobie: czy istnieją ciągi dowolnych rzędów?
Graf de Bruijna Gn rzędu n 2n-1 wierzchołków – słowa binarne (n-1)-literowe krawędzie: a1a2…an-1 → a2…an-10 (etykieta: 0) i a1a2…an-1→a2…an-11 (etykieta: 1) cykle Eulera w grafach de Bruijna odpowiadają ciągom de Bruijna!
Ciekawostka: inny rysunek G4 Ilustracja G4 przypominająca tzw. układy dynamiczne (na podstawie en.wikipedia.org)
Eulerowskość Gn do wierzchołka a1a2…an-1 wchodzą dwie krawędzie: 0a1…an-2 → a1…an-2an-1 1a1…an-2 → a1…an-2an-1 indeg[v] = outdeg[v] silnie spójny (jak dojść z 110 do 011?) eulerowski na mocy kryterium (dowód kryterium potem)
Cykl Eulera ciągiem de Bruijna n kolejnych liter cyklu jednoznacznie wyznacza krawędź grafu. przykład: 1010000110010111 A zatem żadne podsłowo długości n nie powtarza się więcej niż raz!
Cykle Eulera Warunki konieczne i dostateczne: graf skierowany: silnie spójny, indeg[v] = outdeg[v] graf nieskierowany: spójny, 2 | deg[v] Algorytm (graf skierowany): idź, aż się zacyklisz (dlaczego to się zawsze uda?); dobuduj rekurencyjnie cykle w pozostałych silnie spójnych składowych; podoklejaj te cykle do początkowego.
Przykład
Zaczynamy z wierzchołka 0 i znajdujemy jakiś cykl. Przykład cd. Zaczynamy z wierzchołka 0 i znajdujemy jakiś cykl.
Usuwamy znaleziony cykl. Przykład cd. Usuwamy znaleziony cykl.
Przykład cd. Znajdujemy rekurencyjnie cykle Eulera w silnie spójnych składowych pozostałej części grafu.
Przykład cd. Doklejamy cykle, uzyskując cykl Eulera: 0 → 1 → 4 → 5 → 1 → 2 → 6 → 7 → 8 → 2 → 3 → 0
Jak to elegancko zapisać? void euler(int v) { while (v ma nieodwiedzonego sąsiada) { w = sąsiad(v); // usuwamy w euler(w); pisz_na_koniec_wyniku(v); } Wywołanie: euler(0). Dla przykładu: 0 → 1 → 2 → 3 → 0 2 → 6 → 7 → 8 → 2 1 → 2 1 → 4 → 5 → 1 0 → 1 Wynik: 0, 1, 4, 5, 1, 2, 6, 7, 8, 2, 3.
Algorytm Forda Zaleta: łatwiejszy w zapisie Wada: pozwala na wygenerowanie jedynie pewnej klasy ciągów de Bruijna u = 11…1; while (true) { v = u2u3…un; if (było[v0]) u = v1; else u = v0; if (było[u]) break; pisz(un); } Ciąg wygenerowany dla n=4: 0000100110101111. Nie działa, gdy zaczynamy od samych zer: 00001000X. Skąd się ten dziwny algorytm w ogóle wziął?...
Skierowane drzewo rozpinające analogia do wersji nieskierowanej Jeżeli G ma n wierzchołków, to skierowane drzewo rozpinające T ukorzenione w v0 spełnia: T zawiera v-1 krawędzi; z każdego wierzchołka v ≠ v0 wychodzi dokładnie jedna krawędź z T; z v0 nie wychodzi żadna krawędź z T; z każdego wierzchołka v da się dojść do v0 za pomocą krawędzi z T.
Przykład Przykładowy graf z zaznaczonym skierowanym drzewem rozpinającym, ukorzenionym w wierzchołku 0.
Drzewa a cykle Startujemy z v0. Dokładamy kolejne krawędzie, stosując kryterium: krawędzie z T są „zabronione”, czyli używane najpóźniej, jak tylko się da. Pytania „na boku”: Czy w każdym grafie eulerowskim istnieje skierowane drzewo rozpinające? Jak takie drzewo wyznaczyć?
Przykład
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd.
Przykład cd. Znaleźliśmy cykl Eulera!!! 0 → 4 → 1 → 2 → 0 → 1 → 3 → 2 → 5 → 3 → 0
Dlaczego to działa? Ścieżka musi skończyć się w v0 (dlaczego?). Wystarczy pokazać, że wszystkie krawędzie z T leżą na ścieżce. Na końcu wszystkie krawędzie wchodzące do v0 muszą być wykorzystane (bo indeg[v0]=outdeg[v0]), więc także te z T (3 → 0 z rys.). 3 → 0 zostaje wykorzystana jako ostatnia wychodząca z 3. W momencie jej użycia, wszystkie krawędzie wchodzące do 3 muszą być wykorzystane, w tym te z T (5 → 3 i 1 → 3 z rys.) itd. Na koniec dochodzimy do „liści” T. No to OK.
Drzewa a ciągi de Bruijna Podgraf „jedynkowy” grafu G5 – drzewo z pętlą.
Otrzymujemy algorytm Forda! Usuwamy pętlę – otrzymujemy skierowane drzewo rozpinające T. Za pomocą T otrzymujemy dokładnie algorytm Forda!
Ciekawostki Ciąg wygenerowany przez algorytm T jest najmniejszy alfabetycznie wśród ciągów de Bruijna danego rzędu. Związek między cyklami Eulera a skierowanymi drzewami rozpinającymi może być wykorzystany do zliczenia cykli Eulera w danym grafie. Tak wyprowadza się wzór na liczbę ciągów de Bruijna rzędu n: 22^(n-1). Opisane metody są bardzo pamięciożerne. Znanych jest wiele innych metod generowania ciągów de Bruijna, w tym takie, które potrzebują zaledwie O(n) pamięci. O różnych takich ciekawych metodach oraz rozmaitych własnościach ciągów de Bruijna można poczytać... w mojej pracy magisterskiej www.mimuw.edu.pl/~jrad/mgr.pdf (uwaga: praca jest sztywna i nudna)