Zaawansowane algorytmy i struktury danych/Wykład 14: Różnice pomiędzy wersjami
m Zastępowanie tekstu – „<math> ” na „<math>” |
|||
(Nie pokazano 16 wersji utworzonych przez 4 użytkowników) | |||
Linia 3: | Linia 3: | ||
__TOC__ | __TOC__ | ||
W module tym | W module tym zajmiemy się obliczaniem wyrażeń arytmetycznych i równoległymi obliczeniami na drzewach związanymi z tzw. ''kontrakcją drzew''. | ||
W dużym stopniu w module tym możemy zapomnieć o maszynie PRAM i przyjąć, że naszym modelem obliczeń równoległych jest drzewo (lub graf acykliczny). | |||
Czas równoległy to wysokość drzewa, a liczba procesorów to liczba węzłów. Węzły odpowiadają akcjom do wykonania, a drzewo (lub ogólniej graf acykliczny) odpowiada wymuszaniu pewnej kolejności wykonywania akcji. W jednym równoległym kroku możemy wykonać jednocześnie wszystkie te akcje, dla | |||
których akcje poprzedzające je w drzewie (grafie) są już wykonane. | |||
Czasami dana akcja nie musi czekac na zakonczenie wszystkich poprzedzających ja akcji, a może wykonywać jakąś pożyteczną pracę wcześniej, jeśli tylko część informacji potrzebnej do wykonania tej akcji jest obliczona. | |||
Ta część informacji to na przykład obliczona wartość jednego z poprzedników danego węzła. Pomimo, że drugi poprzednik się jeszcze liczy możemy zacząc liczyć częsciowe wyniki związane z tym węzłem. | |||
Zasadniczy problem to jak przekształcić (''spłaszczyć'') | |||
drzewo ''chude'' | |||
(o dużej wysokości) w semantycznie równoważne drzewo o małej wysokości nie zmieniając istotnie rzędu liczby węzłów drzewa. | |||
==Obliczanie wyrażeń arytmetycznych: algorytm jednoczesnych podstawień == | ==Obliczanie wyrażeń arytmetycznych: algorytm jednoczesnych podstawień == | ||
Zakładamy, że wyrażenie zadane jest przez ''program sekwencyjny'' (w skrócie ''PS''). Program taki jest | Zakładamy, że wyrażenie zadane jest przez ''program sekwencyjny'' (w skrócie ''PS''). Program taki jest "sztywną" sekwencją operacji, bez instrukcji warunkowych. PS jest bardzo prostym modelem obliczeń sekwencyjnych. Bardziej precyzyjnie, definiujemy program sekwencyjny jako ciąg instrukcji przypisań postaci <math>x_i := W_i</math>, gdzie <math>W_i</math> jest wyrażeniem arytmetycznym zawierającym <math>O(1)</math> operacji. W przypadku gdy operacje są logiczne, PS nazywamy obwodem logicznym (ang. boolean circuit). Ogólnie problem obliczenia wartości obwodu logiczngo jest P-zupełny, podobnie jest dla arytmetycznych programów sekwencyjnych. | ||
Problemy te są się w klasie NC, gdy graf związany z programem sekwencyjnym jest drzewem, a sam program odpowiada obliczaniu wyrażenia arytmetycznego, co będziemy dalej zakładać. | |||
Zmienne w programie sekwencyjnym są ponumerowane. Jeśli po lewej stronie instrukcji | Zmienne w programie sekwencyjnym są ponumerowane. Jeśli po lewej stronie instrukcji przypisania jest zmienna <math>x_i</math> a po prawej zmienna <math>x_j</math>, to wymagamy, aby <math>j<i</math>. Zakładamy, że struktura obliczeń jest drzewem oraz operacje są ze zbioru <math>\{+,-,*,/\}</math>. | ||
Sekwencyjna | '''Obserwacja''' Program sekwencyjny odpowiada układowi arytmetycznemu w sensie poprzedniego modułu, jednakże motywacją zmiany terminologii ‘’układ arytmetyczny’’ na ‘’PS’’ jest sekwencyjny zapis. . W przypadku układu arytmetycznego, liczba węzłów odpowiada czasowi sekwencyjnemu, a wysokość czasowi równoległemu. | ||
W module tym zajmiemy się, w sensie układu arytmetycznego, transformacją jednego układu na równoważny mu drugi układ, | |||
który ma małą (logarytmiczną) wysokość.<br> | |||
Sekwencyjna pojedyncza operacja podstawiania polega na zastąpieniu zmiennej <math>x_j</math> w danym wyrażeniu <math>W_i</math>, gdzie <math>i>j</math>, przez <math>W_j</math> oraz redukcji wyrażenia po podstawieniu. \myskip Mówimy, że zmienna <math>x_j</math> jest "bezpieczna", gdy jej prawa strona w programie sekwencyjnym (wyrażenie <math>W_j</math>) zawiera co najwyżej jedną zmienną. W przykładzie poniżej 5 pierwszych zmiennych jest bezpiecznych. | |||
{{przyklad||| | {{przyklad||| | ||
Rozważmy następujący program sekwencyjny <math>P</math>: | Rozważmy następujący program sekwencyjny <math>P</math>: | ||
<math>x1 = 1</math>, <math>x2 = 2</math>, <math> x3 = 6</math>, <math>x4 = 4</math>, <math>x5 = x3 + 2</math>, | <math>x1 = 1</math>, <math>x2 = 2</math>, <math>x3 = 6</math>, <math>x4 = 4</math>, <math>x5 = x3 + 2</math>, | ||
<math>x6 = x2 + x1</math>, <math>x7 = 3*x6 + 2*x5</math>, <math>x8 = 2*x7 + x4</math>. | <math>x6 = x2 + x1</math>, <math>x7 = 3*x6 + 2*x5</math>, <math>x8 = 2*x7 + x4</math>. | ||
}} | }} | ||
Przykładowo, po wykonaniu wszystkich bezpiecznych podstawień (w tym wypadku tylko zastąpienie <math>x5</math> przez <math>x3+2</math>) do prawej strony dla x7 otrzymujemy | |||
<center><math>x7 = 3*x6 + 2*x3 + 4</math>. </center> | <center><math>x7 = 3*x6 + 2*x3 + 4</math>. </center> | ||
Operacja ''Reduce'' polega na wykonaniu jednocześnie wszystkich bezpiecznych podstawień we wszystkich wyrażeniach <math>W_i</math>. W programie rozważamy tylko te zmienne, które są istotne z punktu widzenia liczenia ostatecznego wyniku <math>x_n</math>. Zmienne te nazywamy istotnymi | Operacja ''Reduce'' polega na wykonaniu jednocześnie wszystkich bezpiecznych podstawień we wszystkich wyrażeniach <math>W_i</math>. W programie rozważamy tylko te zmienne, które są istotne z punktu widzenia liczenia ostatecznego wyniku <math>x_n</math>. Zmienne te nazywamy istotnymi. Są one osiągalne ze zmiennej <math>x_n</math> w grafie programu. Oznaczmy przez ''Reduce(P)'' nowy program sekwencyjny, bez zmiennych nieistotnych. | ||
{{algorytm|JP|| | {{algorytm|JP|| | ||
Linia 38: | Linia 57: | ||
}} | }} | ||
Historia algorytmu dla przykładowego programu P danego powyżej wygląda następująco, gdzie <math>P=P_0, P_{i+1}= \ | Historia algorytmu dla przykładowego programu P danego powyżej wygląda następująco, gdzie <math>P=P_0, P_{i+1}= \mathit{Reduce}(P_i)</math>. | ||
<math>P_1</math>: <br> | <math>P_1</math>: <br> | ||
Linia 51: | Linia 70: | ||
<math>x8 =54</math>.<br> | <math>x8 =54</math>.<br> | ||
Pokażemy, że algorytm wykonuje <math>O(\log n)</math> iteracji. | Pokażemy, że algorytm wykonuje <math>O(\log n)</math> iteracji. Udowodnimy bardzo precyjne oszacowanie: rozmiar najmniejszego programu sekwencyjnego, który wmaga <math>k</math> iteracji jest równy <math>k</math>-tej liczbie Fibonacciego. | ||
Jest to zaskakująco podobne do analogicznego faktu dla drzew AVL, czas równoległy odpowiada tutaj wysokości drzewa. | |||
== | ==Równoległa kontrakcja drzew== | ||
Grafem obliczeń <math>T=\cal{G}(P)</math> programu sekwencyjnego <math>P</math> jest skierowany graf acykliczny którego węzły odpowiadają | Grafem obliczeń <math>T=\cal{G}(P)</math> programu sekwencyjnego <math>P</math> jest skierowany graf acykliczny, którego węzły odpowiadają zmiennym biorącym aktualnie udział w obliczeniu <math>x_n</math>. Synami węzła <math>x_i</math> (zmiennej) są zmienne występujące w danym momencie w <math>W_i</math>. Graf <math>T</math> zawiera tylko węzły osiągalne z korzenia <math>x_n</math>. Przez rozmiar rozumiemy liczbę węzłów grafu (liczbę zmiennych w przypadku PS). | ||
Zauważmy, że w danej iteracji algorytmu JP rozmiar grafu <math>T</math> może być znacznie mniejszy niż <math>n</math>, ponieważ wiele zmiennych | Zauważmy, że w danej iteracji algorytmu JP rozmiar grafu <math>T</math> może być znacznie mniejszy niż <math>n</math>, ponieważ wiele zmiennych może być nieosiągalnych z korzenia. | ||
Jedna iteracja ''Reduce'' (<math>P_{i}\rightarrow P_{i+1}</math>) odpowiada następującej operacji <math>Contract({\cal{G}}(P_i))</math>, | Jedna iteracja ''Reduce'' (<math>P_{i}\rightarrow P_{i+1}</math>) odpowiada następującej operacji <math>Contract({\cal{G}}(P_i))</math>, patrz rysunek. | ||
<center>[[Grafika:Parallel2-1.png]]<br> Rysunek 1. Składowe operacje jednej iteracji Contract(T): ''Compress'' oraz ''Rake''. </center> | <center>[[Grafika:Parallel2-1.png]]<br> Rysunek 1. Składowe operacje jednej iteracji Contract(T): ''Compress'' oraz ''Rake''. </center> | ||
W rzeczywistości możemy | W rzeczywistości możemy zapomnieć o algorytmie JP i maszynie PRAM. Obliczanie na drzewie jest samo w sobie sensownym modelem obliczeń równoległych. Czas równoległy jest liczbą iteracji, która przekształca początkowe drzewo w drzewo jednoelementowe. | ||
< | Z każdą krawędzią <math>e=(x_i,x_j)</math> grafu <math>\cal{G}(P)</math> jest związana pewna funkcja <math>f_e(x)</math> postaci: | ||
<center><math>f_e(x)= \frac{ax+b}{cx+d}</math></center> | |||
gdzie <math>a,b,c,d</math> są pewnymi stałymi. | |||
Początkowo funkcja ta jest identycznościowa. Jeśli w danym momencie następnikami węzła <math>x_i</math> są <math>x_j,\ x_k</math> oraz <math>e1=(x_i,x_j), e2=(x_i,x_k)</math>, to wartością zmiennej <math>x_i</math> jest: <math>f_{e1}(x_j) \otimes f_{e2}(x_k)</math>, gdzie <math>\otimes</math> jest operacją arytmetyczną odpowiadającą węzłowi <math>x_i</math>. | |||
<center>[[Grafika:Parallel2-2.png]]<br> Rysunek 2. Przykładowe drzewo <math>T</math> wyrażenia | Podstawianie wyrażeń za zmienne odbywa się na funkcjach związanych z krawędziami. Jeśli wartości <math>x_j,x_k</math> są obliczone (zmienne te odpowiadają aktualnie liściom), to wartość <math>f_{e1}(x_j) \otimes f_{e2}(x_k)</math> jest konkretną końcową wartością zmiennej <math>x_i</math>. | ||
Rozważamy też "spłaszczoną" wersję <math>T'=squash(T)</math> drzewa T, odpowiadającą algorytmowi kontrakcji. W drzewie <math>T'</math> wewnętrzne węzły odpowiadają zmiennym programu sekwencyjnego lub krawędziom między zmiennymi. Wartością krawędzi<math>e</math> jest funkcja <math>f_e</math>, reprezentowana przez cztery stałe <math>a,b,c,d</math>. Jeśli krawędź <math>e1=(x,z)</math> jest kompozycją dwóch krawędzi <math>e1=(x,y), e2=(y,z)</math>, to wartością <math>f_e</math> jest kompozycja funkcji <math>f_{e1}, f_{e2}</math>. Kompozycja ta polega na wykonaniu operacji na czterch stałych odpowiadających funkcjom. Jeśli zależność jest tylko od jednej krawędzi, to automatycznie rozumiemy, że funkcja odpowiadająca pominiętej krawędzi jest identycznościowa (a takie są początkowo wszystkie funkcje w początkowym grafie <math>\cal{G}(P)</math>). Na następującym przykładzie pokażemy, jak działa operacja Contract i w jaki sposób związana jest ona z algorytmem JP. | |||
<center>[[Grafika:Parallel2-2.png]]<br> Rysunek 2. Przykładowe drzewo <math>T</math> wyrażenia. Początkowe wartości w liściach są podane w kwadracikach, numery węzłów są w podane w nawiasach. <center> | |||
Linia 82: | Linia 108: | ||
<center>[[Grafika:Parallel2-5.png]]<br> Rysunek 5. | <center>[[Grafika:Parallel2-5.png]]<br> Rysunek 5. "Spłaszczone" drzewo <math>T'=squash(T)</math> liczące to samo, co drzewo T, ale mające mniejszą wysokość (ogólnie logarytmiczną). Drzewo to można jeszcze bardziej spłaszczyć, kompresując łańcuchy dwukrawędziowe (eliminując węzły wewnętrzne mające dokładnie jednego syna. </center> | ||
== Oszacowanie liczby | == Oszacowanie liczby kontrakcji== | ||
Załóżmy, że graf <math>\cal{G}(P)</math> programu | Załóżmy, że graf <math>\cal{G}(P)</math> programu sekwencyjnego P jest drzewem T. Wtedy liczba iteracji algorytmu JP jest równa liczbie kontrakcji drzewa T, po której korzeń T staje się liściem. Przez <math>TIME(n)</math> oznaczmy maksymalną liczbę kontrakcji potrzebnych do zredukowania do jednego węzła drzewa o <math>n</math> węzłach. Niech <math>Fib_n</math> będziem <math>n</math>-tą liczbą Fibonacciego, gdzie <math>Fib_0=1, Fib_1=2</math>. Udowodnimy: | ||
{{twierdzenie||| | {{twierdzenie||| | ||
'''(a)''' <math>TIME(Fib_n)=n</math>. | '''(a)''' <math>TIME(Fib_n)=n</math>. | ||
'''(b)''' <math>Fib_k \le n < Fib_{k+1'''\ \Rightarrow \ TIME(n)=k</math>. | '''(b)''' <math>Fib_k \le n < Fib_{k+1'''\ \Rightarrow \ TIME(n)=k}</math>. | ||
}} | }} | ||
Jako bezpośredni wniosek otrzymujemy: | Jako bezpośredni wniosek otrzymujemy: | ||
<center><math> \log_{\phi}+c \le TIME(n) \le \log_{\phi}+c,\ \ | <center><math>\log_{\phi}+c \le TIME(n) \le \log_{\phi}+c,\ \text{dla pewnej stałej c}</math></center> | ||
gdzie <math>\Phi | gdzie <math>\Phi= \frac{1+\sqrt{5}}{2}</math> jest liczbą tzw. złotego podziału (w przybliżeniu <math>\Phi \approx 1.6</math>). | ||
Udowodnimy najpierw, że <math>TIME(Fib_k)\ge k</math>. Niech <math>T_k</math> będzie <math>k</math>-tym drzewem Fibonacciego, zdefiniowanym na rysunku. Drzewo <math>T_{k+2}</math> powstaje przez utożsamienie korzeni dwóch początkowo rozłącznych wierzchołkowo drzew <math>T_k</math> i <math>T_{k+1}</math> | Udowodnimy najpierw, że <math>TIME(Fib_k)\ge k</math>. Niech <math>T_k</math> będzie <math>k</math>-tym drzewem Fibonacciego, zdefiniowanym na rysunku. Drzewo <math>T_{k+2}</math> powstaje przez utożsamienie korzeni dwóch początkowo rozłącznych wierzchołkowo drzew <math>T_k</math> i <math>T_{k+1}</math> oraz dodanie nowego korzenia. | ||
<center>[[Grafika:Parallel2-6.png]]<br> Rysunek 6. Drzewa Fibonacciego. </center> | <center>[[Grafika:Parallel2-6.png]]<br> Rysunek 6. Drzewa Fibonacciego. </center> | ||
Linia 115: | Linia 141: | ||
}} | }} | ||
Wprowadzamy pojęcie ''statycznego węzła'' | Wprowadzamy pojęcie ''statycznego węzła'' jako liścia, który nigdy nie może być "odcięty", nie możemy usunąć krawędzi prowadzącej do niego. Poza tym operacja kontrakcji działa tak jak poprzednio. Na rysunkach statyczny węzeł jest zaznaczony jako kwadracik. Jeśli drzewo ma dokładnie jeden statyczny węzeł, to po pewnej liczbie kontrakcji otrzymujemy drzewo składające się tylko z korzenia i tego węzła. Oznaczmy przez <math>TIME'(n)</math> maksymalną liczbę kontrakcji dla drzewa mającego n węzłów, które doprowadzają do takiej sytuacji. W liczbę n nie wliczamy węzła statycznego. | ||
Pozostawiamy jako ćwiczenie dowód tego, że <math>TIME'(Fib_k+1)>k</math>. | Pozostawiamy jako ćwiczenie dowód tego, że <math>TIME'(Fib_k+1)>k</math>. | ||
Następujący dosyć oczywisty fakt jest użyteczny w dalszym dowodzie | Następujący dosyć oczywisty fakt jest użyteczny w dalszym dowodzie lematu o kontrakcji. | ||
'''Fakt.''' Niech <math>x\in T</math> | '''Fakt.''' Niech <math>x\in T</math> oraz <math>T',\ T_x</math> będą takie, jak pokazane na Rysunku 7. Jeśli <math>t</math> kontrakcji redukuje <math>T'</math> do drzewa rozmiaru jeden (nie licząc węzła statycznego), to po <math>t</math> kontrakcjach drzewo <math>T</math> redukuje się do pojedyńczego węzła staje się drzewem złożonym tylko z korzenia, którego jedynem następnikiem jest liść lub element drzewa <math>T_x</math>. | ||
<center>[[Grafika:Parallel2-7.png]]<br> Rysunek 7. Graficzna definicja drzew <math>T'</math>, <math>T_x</math>.</center> | <center>[[Grafika:Parallel2-7.png]]<br> Rysunek 7. Graficzna definicja drzew <math>T'</math>, <math>T_x</math>.</center> | ||
Udowodnimy lemat, który będzie rozszerzeniem | Udowodnimy lemat, który będzie rozszerzeniem lematu o kontrakcji. Celowo wzmacniamy tezę, aby ułatwić dowód przez indukcję. | ||
{{lemat| Wzmocniony | {{lemat| Wzmocniony lemat o kontrakcji|| | ||
'''(a)''' <math>n < FIB_k\ \Rightarrow\ TIME(n) < k-1</math>; | '''(a)''' <math>n < FIB_k\ \Rightarrow\ TIME(n) < k-1</math>; | ||
Linia 140: | Linia 166: | ||
'''Dowód punktu (a)''' | '''Dowód punktu (a)''' | ||
Rozważmy drzewo <math>T</math> takie, że <math>|T| < FIB_k</math>. Niech <math>x</math> będzie najniższym (o | Rozważmy drzewo <math>T</math> takie, że <math>|T| < FIB_k</math>. Niech <math>x</math> będzie najniższym (o najmniejszej wysokości) węzłem, takim że <math>|T_x| \ge FIB_{k-1}</math>. Niech <math>p</math>, <math>q</math> będą następnikami <math>x</math>, patrz Rysunek 8. (Jeśli<math>x</math> ma tylko jednego następnika, nazwijmy go <math>p</math>). Zatem | ||
<center><math>|T_ p|, |T_q| < FIB_{k-1}</math></center> | <center><math>|T_ p|, |T_q| < FIB_{k-1}</math></center> | ||
Udowodnimy, że po <math>k-1</math>-szej | Udowodnimy, że po <math>k-1</math>-szej kontrakcji korzeń <math>T</math> staje się liściem. Możliwe są dwa przypadki: | ||
'''Przypadek I''': Po kontrakcji <math>(k-2)</math>-giej węzeł <math>x</math> jest liściem. <br>Z założenia indukcyjnego wynika, że drzewo <math>T1</math> jest zredukowane do korzenia z jednym następnikiem, który jest wewnątrz <math>T_x</math>. Ponieważ wszystkie węzły w <math>T_x</math> stały się liśćmi (gdyż korzeń <math>T_x</math> stał się liściem), to w następnej kontrakcji wykonujemy operację Rake i korzeń całego drzewa staje się liściem. | |||
<center> [[Grafika:Parallel2-8.png]]<br> Rysunek 8. Drzewo <math>T</math> i jego dekompozycja: <math>|T|<FIB_k</math>,<math>|T1|\le FIB_{k-2}</math>, <math>|T_x|\ge FIB_{k-1}</math>, <math>|T_p|<FIB_{k-1}</math>.</center> | |||
'''Przypadek II''': Korzeń drzewa <math>T_x</math> wymaga co najmniej <math>k-1</math> kontrakcji by stał się liściem. <br> Dla drzewa <math>T</math> niech <math>v\notin R</math> będzie dodatkowym węzłem, a <math>R \otimes v</math> będzie nowym drzewem o korzeniu <math>v</math> mającym jednego następnika - korzeń R. Łatwo zauważyć, że co najmniej jedno z drzew <math>T_p\otimes x</math> lub <math>T_q\otimes x</math> wymaga co najmniej <math>k-1</math> kontrakcji, aby korzeń stał się liściem. Przyjmijmy, bez straty ogólności, że jest to pierwsze z nich. | |||
Z założenia indukcyjnego wynika, że <math>|T_p\otimes x| \ge FIB_{k-1}</math> (zatem <math>|T_p|=FIB_{k-1}-1)</math>. Wynika stąd, że drzewo <math>T2</math> z węzłem statycznym (patrz Rysunek 8) jest "małe": <math>|T2| \le FIB_{k-2}</math>. | |||
< | W tej sytuacji z założenia indukcyjnego (b) wynika, że drzewo <math>T2</math> jest zredukowane do korzenia z jednym liściem po <math>k-2</math> kontrakcjach. Wszystkie węzły <math>T_p</math> stają się liścmi po <math>k-2</math> kontrakcjach na mocy założenia indukcyjnego (a). Zatem podobnie jak w przypadku I, korzeń całego drzewa staje się liściem po <math>k-1</math> kontrakcjach. | ||
''' | '''Dowód punktu (b)''' | ||
Przypadek ten rozpatruje się podobnie jak punkt (a), stosując odpowiednią dekompozycję drzewa <math>T</math>. Dowód ten pozostawiamy jako ćwiczenie. | |||
'''Obserwacja.'''Możliwe są różne inne definicje kontrakcji drzew. Na przykład moglibyśmy rozważać, że w jednej iteracji kontrakcji najpierw wykonujemy Rake (usuń liście), a dopiero potem Compress. My wykonywaliśmy to w jednym kroku równocześnie, co wydaje się bardziej naturalne. |
Aktualna wersja na dzień 22:15, 11 wrz 2023
Algorytmy równoległe II
W module tym zajmiemy się obliczaniem wyrażeń arytmetycznych i równoległymi obliczeniami na drzewach związanymi z tzw. kontrakcją drzew. W dużym stopniu w module tym możemy zapomnieć o maszynie PRAM i przyjąć, że naszym modelem obliczeń równoległych jest drzewo (lub graf acykliczny). Czas równoległy to wysokość drzewa, a liczba procesorów to liczba węzłów. Węzły odpowiadają akcjom do wykonania, a drzewo (lub ogólniej graf acykliczny) odpowiada wymuszaniu pewnej kolejności wykonywania akcji. W jednym równoległym kroku możemy wykonać jednocześnie wszystkie te akcje, dla których akcje poprzedzające je w drzewie (grafie) są już wykonane.
Czasami dana akcja nie musi czekac na zakonczenie wszystkich poprzedzających ja akcji, a może wykonywać jakąś pożyteczną pracę wcześniej, jeśli tylko część informacji potrzebnej do wykonania tej akcji jest obliczona. Ta część informacji to na przykład obliczona wartość jednego z poprzedników danego węzła. Pomimo, że drugi poprzednik się jeszcze liczy możemy zacząc liczyć częsciowe wyniki związane z tym węzłem.
Zasadniczy problem to jak przekształcić (spłaszczyć) drzewo chude (o dużej wysokości) w semantycznie równoważne drzewo o małej wysokości nie zmieniając istotnie rzędu liczby węzłów drzewa.
Obliczanie wyrażeń arytmetycznych: algorytm jednoczesnych podstawień
Zakładamy, że wyrażenie zadane jest przez program sekwencyjny (w skrócie PS). Program taki jest "sztywną" sekwencją operacji, bez instrukcji warunkowych. PS jest bardzo prostym modelem obliczeń sekwencyjnych. Bardziej precyzyjnie, definiujemy program sekwencyjny jako ciąg instrukcji przypisań postaci , gdzie jest wyrażeniem arytmetycznym zawierającym operacji. W przypadku gdy operacje są logiczne, PS nazywamy obwodem logicznym (ang. boolean circuit). Ogólnie problem obliczenia wartości obwodu logiczngo jest P-zupełny, podobnie jest dla arytmetycznych programów sekwencyjnych.
Problemy te są się w klasie NC, gdy graf związany z programem sekwencyjnym jest drzewem, a sam program odpowiada obliczaniu wyrażenia arytmetycznego, co będziemy dalej zakładać.
Zmienne w programie sekwencyjnym są ponumerowane. Jeśli po lewej stronie instrukcji przypisania jest zmienna a po prawej zmienna , to wymagamy, aby . Zakładamy, że struktura obliczeń jest drzewem oraz operacje są ze zbioru .
Obserwacja Program sekwencyjny odpowiada układowi arytmetycznemu w sensie poprzedniego modułu, jednakże motywacją zmiany terminologii ‘’układ arytmetyczny’’ na ‘’PS’’ jest sekwencyjny zapis. . W przypadku układu arytmetycznego, liczba węzłów odpowiada czasowi sekwencyjnemu, a wysokość czasowi równoległemu.
W module tym zajmiemy się, w sensie układu arytmetycznego, transformacją jednego układu na równoważny mu drugi układ,
który ma małą (logarytmiczną) wysokość.
Sekwencyjna pojedyncza operacja podstawiania polega na zastąpieniu zmiennej w danym wyrażeniu , gdzie , przez oraz redukcji wyrażenia po podstawieniu. \myskip Mówimy, że zmienna jest "bezpieczna", gdy jej prawa strona w programie sekwencyjnym (wyrażenie ) zawiera co najwyżej jedną zmienną. W przykładzie poniżej 5 pierwszych zmiennych jest bezpiecznych.
Przykład
Rozważmy następujący program sekwencyjny :
, , , , ,
, , .
Przykładowo, po wykonaniu wszystkich bezpiecznych podstawień (w tym wypadku tylko zastąpienie przez ) do prawej strony dla x7 otrzymujemy
Operacja Reduce polega na wykonaniu jednocześnie wszystkich bezpiecznych podstawień we wszystkich wyrażeniach . W programie rozważamy tylko te zmienne, które są istotne z punktu widzenia liczenia ostatecznego wyniku . Zmienne te nazywamy istotnymi. Są one osiągalne ze zmiennej w grafie programu. Oznaczmy przez Reduce(P) nowy program sekwencyjny, bez zmiennych nieistotnych.
Algorytm JP
(Algorytm Jednoczesnych-Podstawien)
repeat {Operacja Reduce}
for each do in parallel
wykonaj jednoczesnie wszystkie bezpieczne podstawienia w .
until obliczone;
Historia algorytmu dla przykładowego programu P danego powyżej wygląda następująco, gdzie .
:
; ;
;
.
:
, .
:
.
Pokażemy, że algorytm wykonuje iteracji. Udowodnimy bardzo precyjne oszacowanie: rozmiar najmniejszego programu sekwencyjnego, który wmaga iteracji jest równy -tej liczbie Fibonacciego. Jest to zaskakująco podobne do analogicznego faktu dla drzew AVL, czas równoległy odpowiada tutaj wysokości drzewa.
Równoległa kontrakcja drzew
Grafem obliczeń programu sekwencyjnego jest skierowany graf acykliczny, którego węzły odpowiadają zmiennym biorącym aktualnie udział w obliczeniu . Synami węzła (zmiennej) są zmienne występujące w danym momencie w . Graf zawiera tylko węzły osiągalne z korzenia . Przez rozmiar rozumiemy liczbę węzłów grafu (liczbę zmiennych w przypadku PS).
Zauważmy, że w danej iteracji algorytmu JP rozmiar grafu może być znacznie mniejszy niż , ponieważ wiele zmiennych może być nieosiągalnych z korzenia.
Jedna iteracja Reduce () odpowiada następującej operacji , patrz rysunek.

Rysunek 1. Składowe operacje jednej iteracji Contract(T): Compress oraz Rake.
W rzeczywistości możemy zapomnieć o algorytmie JP i maszynie PRAM. Obliczanie na drzewie jest samo w sobie sensownym modelem obliczeń równoległych. Czas równoległy jest liczbą iteracji, która przekształca początkowe drzewo w drzewo jednoelementowe.
Z każdą krawędzią grafu jest związana pewna funkcja postaci:
gdzie są pewnymi stałymi.
Początkowo funkcja ta jest identycznościowa. Jeśli w danym momencie następnikami węzła są oraz , to wartością zmiennej jest: , gdzie jest operacją arytmetyczną odpowiadającą węzłowi .
Podstawianie wyrażeń za zmienne odbywa się na funkcjach związanych z krawędziami. Jeśli wartości są obliczone (zmienne te odpowiadają aktualnie liściom), to wartość jest konkretną końcową wartością zmiennej .
Rozważamy też "spłaszczoną" wersję drzewa T, odpowiadającą algorytmowi kontrakcji. W drzewie wewnętrzne węzły odpowiadają zmiennym programu sekwencyjnego lub krawędziom między zmiennymi. Wartością krawędzi jest funkcja , reprezentowana przez cztery stałe . Jeśli krawędź jest kompozycją dwóch krawędzi , to wartością jest kompozycja funkcji . Kompozycja ta polega na wykonaniu operacji na czterch stałych odpowiadających funkcjom. Jeśli zależność jest tylko od jednej krawędzi, to automatycznie rozumiemy, że funkcja odpowiadająca pominiętej krawędzi jest identycznościowa (a takie są początkowo wszystkie funkcje w początkowym grafie ). Na następującym przykładzie pokażemy, jak działa operacja Contract i w jaki sposób związana jest ona z algorytmem JP.

Rysunek 2. Przykładowe drzewo wyrażenia. Początkowe wartości w liściach są podane w kwadracikach, numery węzłów są w podane w nawiasach.

Rysunek 3. Drzewo razem z dodatkowymi funkcjami na krawędziach. Brak funkcji oznacza funkcję identycznościową.

Rysunek 4. Kolejne drzewa . Algorytm wykonuje 4 kontrakcje.

Rysunek 5. "Spłaszczone" drzewo liczące to samo, co drzewo T, ale mające mniejszą wysokość (ogólnie logarytmiczną). Drzewo to można jeszcze bardziej spłaszczyć, kompresując łańcuchy dwukrawędziowe (eliminując węzły wewnętrzne mające dokładnie jednego syna.
Oszacowanie liczby kontrakcji
Załóżmy, że graf programu sekwencyjnego P jest drzewem T. Wtedy liczba iteracji algorytmu JP jest równa liczbie kontrakcji drzewa T, po której korzeń T staje się liściem. Przez oznaczmy maksymalną liczbę kontrakcji potrzebnych do zredukowania do jednego węzła drzewa o węzłach. Niech będziem -tą liczbą Fibonacciego, gdzie . Udowodnimy:
Twierdzenie
(a) .
(b) .
Jako bezpośredni wniosek otrzymujemy:
gdzie jest liczbą tzw. złotego podziału (w przybliżeniu ).
Udowodnimy najpierw, że . Niech będzie -tym drzewem Fibonacciego, zdefiniowanym na rysunku. Drzewo powstaje przez utożsamienie korzeni dwóch początkowo rozłącznych wierzchołkowo drzew i oraz dodanie nowego korzenia.

Rysunek 6. Drzewa Fibonacciego.
Pozostawiamy jako ćwiczenie uzasadnienie tego, że oraz .
Wynika stąd, że .
Przejdziemy teraz do dowodu następującego faktu.
Lemat o kontrakcji
.
Wprowadzamy pojęcie statycznego węzła jako liścia, który nigdy nie może być "odcięty", nie możemy usunąć krawędzi prowadzącej do niego. Poza tym operacja kontrakcji działa tak jak poprzednio. Na rysunkach statyczny węzeł jest zaznaczony jako kwadracik. Jeśli drzewo ma dokładnie jeden statyczny węzeł, to po pewnej liczbie kontrakcji otrzymujemy drzewo składające się tylko z korzenia i tego węzła. Oznaczmy przez maksymalną liczbę kontrakcji dla drzewa mającego n węzłów, które doprowadzają do takiej sytuacji. W liczbę n nie wliczamy węzła statycznego.
Pozostawiamy jako ćwiczenie dowód tego, że .
Następujący dosyć oczywisty fakt jest użyteczny w dalszym dowodzie lematu o kontrakcji.
Fakt. Niech oraz będą takie, jak pokazane na Rysunku 7. Jeśli kontrakcji redukuje do drzewa rozmiaru jeden (nie licząc węzła statycznego), to po kontrakcjach drzewo redukuje się do pojedyńczego węzła staje się drzewem złożonym tylko z korzenia, którego jedynem następnikiem jest liść lub element drzewa .

Rysunek 7. Graficzna definicja drzew , .
Udowodnimy lemat, który będzie rozszerzeniem lematu o kontrakcji. Celowo wzmacniamy tezę, aby ułatwić dowód przez indukcję.
Lemat Wzmocniony lemat o kontrakcji
(a) ;
(b).
Dowód przeprowadzamy przez indukcję ze względu na . Dla , łatwo sprawdzić, że teza zachodzi. Załóżmy teraz, że teza zachodzi dla wszystich mniejszych niż dane.
Dowód punktu (a)
Rozważmy drzewo takie, że . Niech będzie najniższym (o najmniejszej wysokości) węzłem, takim że . Niech , będą następnikami , patrz Rysunek 8. (Jeśli ma tylko jednego następnika, nazwijmy go ). Zatem
Udowodnimy, że po -szej kontrakcji korzeń staje się liściem. Możliwe są dwa przypadki:
Przypadek I: Po kontrakcji -giej węzeł jest liściem.
Z założenia indukcyjnego wynika, że drzewo jest zredukowane do korzenia z jednym następnikiem, który jest wewnątrz . Ponieważ wszystkie węzły w stały się liśćmi (gdyż korzeń stał się liściem), to w następnej kontrakcji wykonujemy operację Rake i korzeń całego drzewa staje się liściem.

Rysunek 8. Drzewo i jego dekompozycja: ,, , .
Przypadek II: Korzeń drzewa wymaga co najmniej kontrakcji by stał się liściem.
Dla drzewa niech będzie dodatkowym węzłem, a będzie nowym drzewem o korzeniu mającym jednego następnika - korzeń R. Łatwo zauważyć, że co najmniej jedno z drzew lub wymaga co najmniej kontrakcji, aby korzeń stał się liściem. Przyjmijmy, bez straty ogólności, że jest to pierwsze z nich.
Z założenia indukcyjnego wynika, że (zatem . Wynika stąd, że drzewo z węzłem statycznym (patrz Rysunek 8) jest "małe": .
W tej sytuacji z założenia indukcyjnego (b) wynika, że drzewo jest zredukowane do korzenia z jednym liściem po kontrakcjach. Wszystkie węzły stają się liścmi po kontrakcjach na mocy założenia indukcyjnego (a). Zatem podobnie jak w przypadku I, korzeń całego drzewa staje się liściem po kontrakcjach.
Dowód punktu (b)
Przypadek ten rozpatruje się podobnie jak punkt (a), stosując odpowiednią dekompozycję drzewa . Dowód ten pozostawiamy jako ćwiczenie.
Obserwacja.Możliwe są różne inne definicje kontrakcji drzew. Na przykład moglibyśmy rozważać, że w jednej iteracji kontrakcji najpierw wykonujemy Rake (usuń liście), a dopiero potem Compress. My wykonywaliśmy to w jednym kroku równocześnie, co wydaje się bardziej naturalne.