Skuteczna kompresja nieoznakowanych drzew

20

Rozważ nieoznakowane, ukorzenione drzewa binarne. Możemy skompresować takich drzew: gdy istnieją wskaźniki do poddrzew i T ' z T = T ' (ustne = jak równość strukturalne), możemy zapisać (wlog) T i zastąpić wszystkie wskaźniki do T ' z wskazówki dla T . Zobacz odpowiedź uli na przykład.TTT=T=TTT

Podaj algorytm, który przyjmuje drzewo w powyższym sensie jako dane wejściowe i oblicza (minimalną) liczbę węzłów pozostałych po kompresji. Algorytm powinien działać w czasie (w modelu kosztu jednolitego) z n liczbą węzłów na wejściu.O(nlogn)n

To było pytanie egzaminacyjne i nie byłem w stanie znaleźć dobrego rozwiązania, ani go nie widziałem.

Raphael
źródło
A jaki jest „koszt”, „czas”, elementarna operacja tutaj? Liczba odwiedzonych węzłów? Liczba krawędzi przemierzonych? A jak określa się rozmiar danych wejściowych?
uli
Ta kompresja drzewa jest przykładem użycia skrótu . Nie jestem pewien, czy prowadzi to do ogólnej metody liczenia.
Gilles 'SO - przestań być zły'
@uli Wyjaśniłem, co to jest . Myślę jednak, że „czas” jest wystarczająco konkretny. W ustawieniach niebieżnych jest to równoważne z liczeniem operacji, co w ujęciu Landaua jest równoważne zliczaniu najczęściej wykonywanej operacji elementarnej. n
Raphael
@Raphael Oczywiście mogę zgadywać, jaka powinna być zamierzona elementarna operacja i prawdopodobnie wybiorę to samo, co wszyscy inni. Ale i wiem, że jestem pedantyczny tutaj, za każdym razem, gdy podano „granice czasowe”, ważne jest, aby określić, co się liczy. Czy to zamiana, porównywanie, dodawanie, dostęp do pamięci, sprawdzane węzły, trawersowane krawędzie, nazywacie to. To tak, jakby pominąć jednostkę miary w fizyce. Czy to lub 1010kg ? I przypuszczam, że dostęp do pamięci jest prawie zawsze najczęstszą operacją. 10ms
uli
@uli Są to szczegóły, które ma przekazać „model kosztów jednolitych”. Dokładne określenie, jakie operacje są elementarne, jest bolesne, ale w 99,99% przypadków (w tym tym) nie ma dwuznaczności. Klasy złożoności zasadniczo nie mają jednostek, nie mierzą czasu potrzebnego do wykonania jednej instancji, ale sposób, w jaki czas ten zmienia się w miarę powiększania się danych wejściowych.
Gilles 'SO - przestań być zły'

Odpowiedzi:

10

Tak, możesz wykonać tę kompresję w czasie , ale nie jest to łatwe :) Najpierw dokonamy pewnych obserwacji, a następnie przedstawimy algorytm. Zakładamy, że drzewo początkowo nie jest skompresowane - nie jest to tak naprawdę potrzebne, ale ułatwia analizę.O(nlogn)

Po pierwsze, charakteryzujemy indukcyjnie „równość strukturalną”. Niech i T ' będą dwoma (pod) drzewami. Jeśli zarówno T, jak i T ' są zerowymi drzewami (w ogóle nie mają wierzchołków), są one strukturalnie równoważne. Jeśli oba T i T ' nie są zerowymi drzewami, to są one strukturalnie równoważne, jeśli ich lewe dzieci są strukturalnie równoważne, a ich prawe dzieci są strukturalnie równoważne. „Równoważność strukturalna” to minimalny stały punkt nad tymi definicjami.TTTTTT

Na przykład dowolne dwa węzły liścia są strukturalnie równoważne, ponieważ oba mają drzewa zerowe jako oba swoje dzieci, które są strukturalnie równoważne.

Ponieważ denerwujące jest stwierdzenie „ich lewe dzieci są strukturalnie równoważne, podobnie jak ich prawe dzieci”, często mówimy „ich dzieci są strukturalnie równoważne” i zamierzamy to samo. Zauważ też, że czasami mówimy „ten wierzchołek”, gdy mamy na myśli „poddrzew zakorzeniony w tym wierzchołku”.

dd+1O(n2)

{1,2,3,,n}

nO(n)

Najpierw kompresujemy wszystkie liście (możemy je znaleźć na liście z wierzchołkami o głębokości 0) w jednym wierzchołku. Przypisujemy temu wierzchołkowi identyfikator. Kompresja dwóch wierzchołków odbywa się poprzez przekierowanie elementu nadrzędnego jednego z wierzchołków, tak aby wskazywał na drugi wierzchołek.

dd

Iterujemy wszystkie listy z węzłami o jednakowej głębokości od małej głębokości do dużej głębokości. Dla każdego poziomu tworzymy listę par liczb całkowitych, gdzie każda para odpowiada identyfikatorom potomków niektórych wierzchołków na tym poziomie. Mamy dwa wierzchołki na tym poziomie, które są strukturalnie równoważne, jeśli ich odpowiadające sobie pary liczb całkowitych są równe. Korzystając z porządku leksykograficznego, możemy je posortować i uzyskać zestawy równych par liczb całkowitych. Kompresujemy te zestawy do pojedynczych wierzchołków jak wyżej i nadamy im identyfikatory.

O(n)nO(nlogn)

Alex ten Brink
źródło
Nie przeczytałem szczegółowo twojej odpowiedzi, ale myślę, że na nowo odkryłeś zużycie skrótów, z dziwnym, specyficznym dla problemu sposobem wyszukiwania węzłów.
Gilles „SO- przestań być zły”
@Alex „dzieci ściśle mniejszego degree” stopnia prawdopodobnie powinny być depth? I mimo, że drzewa CS rosną w dół, „wysokość drzewa” jest mniej myląca niż „głębokość drzewa”.
uli
Niezła odpowiedź. Wydaje mi się, że powinien istnieć sposób na obejście sortowania. Mój drugi komentarz do odpowiedzi @Gilles jest również ważny tutaj.
Raphael
@uli: tak, masz rację, poprawiłem to (nie jestem pewien, dlaczego pomyliłem te dwa słowa). Wysokość i głębokość to dwie subtelnie różne koncepcje, a ja potrzebowałem tej drugiej :) Pomyślałem, że wolę trzymać się konwencjonalnej „głębokości” niż mylić wszystkich, zamieniając je.
Alex ten Brink
4

Kompresowanie niemodyfikowalnej struktury danych, aby nie powielała żadnego strukturalnie równego podtermu, znane jest jako sumowanie skrótów . Jest to ważna technika zarządzania pamięcią w programowaniu funkcjonalnym. Hash Consing to rodzaj systematycznego zapamiętywania struktur danych.

nO(nlg(n))

Rozważę drzewa jako mające następującą strukturę (zapisaną tutaj w składni Haskell):

data Tree = Leaf
          | Node Tree Tree

nodes:T×TNTNT=N{}

Możemy wykorzystać strukturę danych w czasie logarytmicznym nodes, na przykład zrównoważone drzewo wyszukiwania binarnego. Poniżej wywołam lookup nodesoperację, która wyszukuje klucz w nodesstrukturze danych, oraz insert nodesoperację, która dodaje wartość do nowego klucza i zwraca ten klucz.

Teraz przechodzimy przez drzewo i dodajemy węzły. Chociaż piszę w pseudokodzie podobnym do Haskella, będę traktować nodesjako zmienną globalną zmienną; będziemy tylko do niego dodawać, ale wstawki muszą być w całości gwintowane. addFunkcja recurses na drzewie, dodając do jego poddrzewa na nodesmapie i zwraca identyfikator korzenia.

insert (p1,p2) =
add Leaf = $\ell$
add (Node t1 t2) =
    let p1 = add t1
    let p2 = add t2
    case lookup nodes (p1,p2) of
      Nothing -> insert nodes (p1,p2)
      Just p -> p

Liczba insertwywołań, która jest również ostatecznym rozmiarem nodesstruktury danych, to liczba węzłów po maksymalnej kompresji. (W razie potrzeby dodaj jedno puste drzewo).

Gilles „SO- przestań być zły”
źródło
nO(nlg(n))nodes
Zastanawiałem się tylko nad strukturami mieszania podstruktur do liczb w taki sposób, aby niezależne obliczanie wartości skrótu dla tego samego drzewa zawsze dawało ten sam wynik. Twoje rozwiązanie również jest w porządku, pod warunkiem, że mamy zmienne struktury danych w naszych rękach. Myślę jednak, że można to odrobinę wyczyścić; przeplatanie inserti addpowinno być wyraźnie określone, i należy nadać funkcję, która faktycznie rozwiązuje problem, imho.
Raphael
1
@Raphael Hash Consing opiera się na skończonej strukturze mapy ponad krotkami wskaźników / identyfikatorów, możesz to zaimplementować z logarytmicznym czasem wyszukiwania i dodawania (np. Ze zrównoważonym drzewem wyszukiwania binarnego). Moje rozwiązanie nie wymaga modyfikacji; Robię nodesto zmienny zmienną dla wygody, ale można wkręcić ją na wskroś. Nie zamierzam podawać pełnego kodu, to nie jest SO.
Gilles „SO- przestań być zły”
1
@Raphael Hashing struktury, w przeciwieństwie do przypisywania im dowolnych liczb, są nieco podejrzane. W modelu kosztu jednolitego można zakodować wszystko w dużą liczbę całkowitą i wykonywać na nim operacje o stałym czasie, co nie jest realistyczne. W prawdziwym świecie można używać skrótów kryptograficznych, aby de facto mapować jeden na jeden od nieskończonych zestawów do skończonego zakresu liczb całkowitych, ale są one powolne. Jeśli używasz sumy kontrolnej innej niż kryptograficzna jako skrótu, musisz pomyśleć o kolizjach.
Gilles „SO- przestań być zły”
3

Oto kolejny pomysł, który ma na celu (iniekcyjne) kodowanie struktury drzew w liczbach, a nie tylko ich arbitralne oznaczanie. W tym celu wykorzystujemy fakt, że faktoryzacja pierwsza dowolnej liczby jest wyjątkowa.

EN(l,r)lrN(E,E)

f(E)=0f(N(l,r))=2f(l)3f(r)

f

To ostatnie założenie dotyczy różnych maszyn; w takim przypadku wolałoby się zastosować coś podobnego do funkcji parowania Cantora zamiast potęgowania.

O(nlogn)O(nlogn)

Raphael
źródło
1

Ponieważ zdjęcia nie są dozwolone w komentarzach:

wprowadź opis zdjęcia tutaj

w lewym górnym rogu: drzewo wejściowe

u góry po prawej: poddrzewa zakorzenione w węzłach 5 i 7 są również izomorficzne.

dolny lewy i prawy: skompresowane drzewa nie są jednoznacznie zdefiniowane.

7+5|T|6+|T|

uli
źródło
To jest rzeczywiście przykład pożądanej operacji, dzięki. Pamiętaj, że twoje końcowe przykłady są identyczne, jeśli nie rozróżniasz oryginalnych i dodanych odniesień.
Raphael
-1

Edycja: Czytam pytanie, ponieważ T i T 'były dziećmi tego samego rodzica. Użyłem również definicji kompresji jako rekurencyjnej, co oznacza, że ​​można skompresować dwa wcześniej poddane kompresji poddrzewa. Jeśli to nie jest prawdziwe pytanie, moja odpowiedź może nie działać.

O(nlogn)T(n)=2T(n/2)+cn

def Comp(T):
   if T == null:
     return 0
   leftCount = Comp(T.left)
   rightCount = Comp(T.right)
   if leftCount == rightCount:
     if hasSameStructure(T.left, T.right):
       T.right = T.left
       return leftCount + 1
     else
       return leftCount + rightCount + 1    

Gdzie hasSameStructure()jest funkcja, która porównuje dwa już skompresowane poddrzewa w czasie liniowym, aby sprawdzić, czy mają dokładnie taką samą strukturę. Pisanie liniowej funkcji rekurencyjnej w czasie, która przechodzi przez każdą i sprawdza, czy jedno poddrzewo ma pozostawione dziecko za każdym razem, gdy robi to drugie itd., Nie powinno być trudne.

nnr

T(n)=T(n1)+T(n2)+O(1) if nnr
2T(n/2)+O(n) otherwise
Joe
źródło
Co jeśli poddrzewa nie są rodzeństwem? Dbanie o ((T1, T1), (T2, T1)) T1 można zapisać dwa razy, używając wskaźnika dwa trzeciego wystąpienia.
uli
TT
Pytania merly stwierdzają, że dwa podteksty są identyfikowane jako izomorficzne. Nic nie mówi się o tym, że mają tego samego rodzica. Jeśli poddrzewo T1 pojawia się trzy razy w drzewie, tak jak w moim poprzednim przykładzie ((T1, T1), (T1, T2)), dwa wystąpienia można skompresować, wskazując na trzeci orcurence.
uli