Tłumaczenie kodu na matematykę
Biorąc pod uwagę (mniej lub bardziej) formalną semantykę operacyjną , możesz dosłownie przetłumaczyć (pseudo-) kod algorytmu na wyrażenie matematyczne, które daje wynik, pod warunkiem, że możesz manipulować tym wyrażeniem w użytecznej formie. Działa to dobrze w przypadku dodatkowych miar kosztów, takich jak liczba porównań, zamiany, zestawienia, dostęp do pamięci, cykle niektórych abstrakcyjnych potrzeb maszyny i tak dalej.
Przykład: Porównania w Bubblesort
Rozważ ten algorytm, który sortuje daną tablicę A
:
bubblesort(A) do 1
n = A.length; 2
for ( i = 0 to n-2 ) do 3
for ( j = 0 to n-i-2 ) do 4
if ( A[j] > A[j+1] ) then 5
tmp = A[j]; 6
A[j] = A[j+1]; 7
A[j+1] = tmp; 8
end 9
end 10
end 11
end 12
Powiedzmy, że chcemy przeprowadzić zwykłą analizę algorytmu sortowania, czyli policzyć liczbę porównań elementów (wiersz 5). Od razu zauważamy, że ta ilość nie zależy od zawartości tablicy A
, tylko od jej długości . Możemy więc przetłumaczyć (zagnieżdżone) pętle dosłownie na (zagnieżdżone) sumy; zmienna pętli staje się zmienną sumującą, a zakres jest przenoszony. Otrzymujemy:nfor
,docmp( n ) = ∑ja= 0n- 2∑jot= 0n -i - 21 = ⋯ = n ( n - 1 )2)= ( n2))
gdzie to koszt każdej realizacji linii 5 (którą liczymy).1
Przykład: zamiana w Bubblesort
Przez oznaczę podprogram składający się z wierszy do i przez C i , j koszty wykonania tego podprogramu (jeden raz).P.ja , ji
j
doja , j
Powiedzmy, że chcemy policzyć swapy , czyli jak często jest wykonywane. Jest to „blok podstawowy”, czyli podprogram, który jest zawsze wykonywany atomowo i ma pewien stały koszt (tutaj 1 ). Kontrakty na takie bloki to jedno z przydatnych uproszczeń, które często stosujemy bez zastanowienia się nad tym.P.6 , 81
Z podobnym tłumaczeniem jak powyżej dochodzimy do następującej formuły:
.dozamiana( A ) = ∑i = 0n - 2∑j = 0n - i - 2do5 , 9( A( i , j ))
oznacza stan tablicy przed ( i , j ) -tą iteracją P 5 , 9 .ZA( i , j )( i , j )P.5 , 9
Zauważ, że używam zamiast n jako parametru; wkrótce zobaczymy dlaczego. Nie dodam i i j jako parametrów C 5 , 9, ponieważ koszty nie zależą od nich tutaj ( to znaczy w modelu kosztu jednolitego ); w ogóle mogą.ZAnjajotdo5 , 9
Oczywiście koszty zależą od zawartości A (wartości, a konkretnie), więc musimy to uwzględnić. Teraz stajemy przed wyzwaniem: w jaki sposób „rozpakowujemy” C 5 , 9 ? Cóż, możemy uczynić zależność od treści A wyraźnym:P.5 , 9ZAA[j]
A[j+1]
do5 , 9ZA
.do5 , 9( A( i , j )) = C5( A( i , j )) + { 10, A( i , j )[ j ] > A( i , j )[ j + 1 ], jeszcze
W przypadku dowolnej tablicy wejściowej koszty te są dobrze określone, ale chcemy bardziej ogólnego zestawienia; musimy przyjąć silniejsze założenia. Przeanalizujmy trzy typowe przypadki.
Najgorszy przypadek
Wystarczy spojrzeć na sumę i zauważyć, że , możemy znaleźć trywialną górną granicę kosztów:do5 , 9( A( i , j )) ∈ { 0 , 1 }
.dozamiana( A ) ≤ ∑i = 0n - 2∑j = 0n - i - 21 = n ( n - 1 )2)= ( n2))
Ale czy może się to zdarzyć , tj. Czy osiągnięto dla tej górnej granicy? Jak się okazuje, tak: jeśli wprowadzimy odwrotnie posortowaną tablicę odrębnych parami elementów, każda iteracja musi wykonać swap¹. Dlatego wyprowadziliśmy dokładną liczbę najgorszych przypadków zamiany Bubblesort.ZA
Najlepszy przypadek
I odwrotnie, istnieje trywialna dolna granica:
.dozamiana( A ) ≥ ∑i = 0n - 2∑j = 0n - i - 20 = 0
Może się to również zdarzyć: w tablicy, która jest już posortowana, Bubblesort nie wykonuje pojedynczej zamiany.
Przeciętny przypadek
Najgorszy i najlepszy przypadek otwiera spore luki. Ale jaka jest typowa liczba zamian? Aby odpowiedzieć na to pytanie, musimy zdefiniować, co oznacza „typowy”. Teoretycznie nie mamy powodu, aby preferować jeden wkład nad drugim, dlatego zwykle zakładamy jednolity rozkład wszystkich możliwych danych wejściowych, to znaczy każde wejście jest jednakowo prawdopodobne. Ograniczamy się do tablic z odrębnymi parami elementów, a zatem zakładamy model losowej permutacji .
Następnie możemy przepisać nasze koszty w ten sposób²:
E [ Czamiana] = 1n !∑ZA∑i = 0n - 2∑j = 0n - i - 2do5 , 9( A( i , j ))
Teraz musimy wyjść poza zwykłą manipulację sumami. Patrząc na algorytm, zauważamy, że każda zamiana usuwa dokładnie jedną inwersję w (wymieniamy tylko sąsiadów3). Oznacza to, że liczba wykonywanych na swapy A jest dokładnie liczba inwersji inw ( A ) z . W ten sposób możemy zastąpić dwie wewnętrzne sumy i uzyskaćZAZAinv( A )ZA
.E [ Czamiana] = 1n !∑ZAinv( A )
Na szczęście dla nas ustalono średnią liczbę inwersji
E [ Czamiana] = 12)⋅ ( n2))
co jest naszym końcowym wynikiem. Należy pamiętać, że jest to dokładnie połowa najgorszych kosztów.
- Zauważ, że algorytm został starannie sformułowany, aby
i = n-1
nie została wykonana „ostatnia” iteracja z zewnętrzną pętlą, która nigdy nic nie robi.
- „ ” jest notacją matematyczną dla „wartości oczekiwanej”, która tutaj jest tylko średnią.mi
- Uczymy się po drodze, że żaden algorytm, który zamienia tylko sąsiednie elementy, nie może być asymptotycznie szybszy niż Bubblesort (nawet średnio) - liczba inwersji jest dolną granicą dla wszystkich takich algorytmów. Dotyczy to np. Sortowania wstawiania i sortowania wyboru .
Ogólna metoda
W przykładzie widzieliśmy, że musimy przełożyć strukturę kontroli na matematykę; Przedstawię typowy zestaw reguł tłumaczenia. Zauważyliśmy również, że koszt dowolnego podprogramu może zależeć od aktualnego stanu , czyli (z grubsza) bieżących wartości zmiennych. Ponieważ algorytm (zwykle) modyfikuje stan, ogólna metoda jest nieco trudna do odnotowania. Jeśli poczujesz się zdezorientowany, sugeruję, abyś wrócił do przykładu lub sam wymyślił.
Oznaczamy bieżącym stanem (wyobraź sobie, że jest to zestaw przypisań zmiennych). Kiedy wykonujemy program zaczynający się w stanie ψ , kończymy w stanie ψ / P (pod warunkiem, że zakończy się).ψP
ψψ / PP
Indywidualne oświadczenia
Biorąc pod uwagę tylko jedną instrukcję S;
, przypisujesz jej koszt . Zazwyczaj będzie to stała funkcja.doS.( ψ )
Wyrażenia
Jeśli masz wyrażenie E
formularza E1 ∘ E2
(powiedzmy, wyrażenie arytmetyczne, gdzie ∘
może być dodawanie lub mnożenie, sumujesz koszty rekurencyjnie:
.domi( ψ ) = c∘+ C.mi1( ψ ) + Cmi2)( ψ )
Zauważ, że
- koszt operacji może nie być stały, ale zależy od wartości E 1 i E 2 orazdo∘mi1mi2)
- ocena wyrażeń może zmienić stan w wielu językach,
więc być może będziesz musiał być elastyczny z tą regułą.
Sekwencja
Biorąc pod uwagę program P
jako sekwencję programów Q;R
, dodajesz koszty do
.doP.( ψ ) = CQ( ψ ) + CR( ψ / Q )
Warunkowe
Biorąc pod uwagę program P
formularza if A then Q else R end
, koszty zależą od stanu:
doP.( ψ ) = CZA( ψ ) + { CQ( ψ / A )doR( ψ / A ), A ocenia się jako prawda pod ψ, jeszcze
Ogólnie rzecz biorąc, ocena A
może bardzo dobrze zmienić stan, stąd aktualizacja kosztów poszczególnych oddziałów.
For-Loops
Biorąc pod uwagę program P
formularza for x = [x1, ..., xk] do Q end
, przypisz koszty
doP.( ψ ) = cinit_for+ ∑i = 1kdostep_for+ C.Q( ψja∘ { x : = x i } )
gdzie jest stanem przed przetwarzaniem wartości , tj. po iteracji z ustawieniem na ..., .ψjaQ
xi
x
x1
xi-1
Zwróć uwagę na dodatkowe stałe dla utrzymania pętli; należy utworzyć zmienną pętli ( ) i przypisać jej wartości ( c step_for ). Jest to istotne, ponieważdoinit_fordostep_for
- obliczanie następnego
xi
może być kosztowne i
for
-loop z pustym korpusie (np po uproszczeniu w zachodzącym z określonym kosztem w najlepszym przypadku) nie ma zerowe koszty, jeżeli spełnia ona iteracji.
Podczas pętli
Biorąc pod uwagę program P
formularza while A do Q end
, przypisz koszty
doP.( ψ ) = CZA( ψ ) + { 0doQ( ψ / A ) + CP.( ψ / A ; Q ), A ocenia na fałsz pod ψ, jeszcze
Po sprawdzeniu algorytmu ta powtarzalność często może być ładnie przedstawiona jako suma podobna do sumy dla pętli for.
Przykład: Rozważ ten krótki algorytm:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
Stosując regułę, otrzymujemy
do1 , 4( { i : = i0; x : = x0} ) = c<+ { 0do+ =+ c/+ C.1 , 4( { i : = i0+ 1 ; x : = ⌊ x0/ 2⌋}), x0≤ 0, jeszcze
do…i
x
do1 , 4i
do1 , 4( x ) = { c>do>+ c+ =+ c/+ C.1 , 4( ⌊ x / 2 ⌋ ), x ≤ 0, jeszcze
To rozwiązuje z podstawowych środków do
do1 , 4( ψ ) = ⌈ log2)ψ ( x ) ⌉ ⋅ ( c>+ c+ =+ c/) + c>
ψ = { … , x : = 5 , … }ψ ( x ) = 5
Wywołania procedur
Biorąc pod uwagę program P
formularza M(x)
dla niektórych parametrów, x
gdzie M
jest procedura z (nazwanym) parametrem p
, przypisz koszty
doP.( ψ ) = cpołączenie+ C.M.( ψglob∘ { p : = x } )
dopołączenieψ
Zastanawiam się nad niektórymi problemami semantycznymi, które możesz mieć z tym państwem tutaj. Będziesz chciał rozróżnić stan globalny i takie lokalne wywołania procedur. Załóżmy, że przekazujemy tutaj tylko stan globalny i M
otrzymujemy nowy stan lokalny, inicjowany przez ustawienie wartości p
na x
. Ponadto x
może być wyrażeniem, które (zwykle) zakładamy, że zostanie ocenione przed przekazaniem.
Przykład: Rozważ procedurę
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
Zgodnie z regułami otrzymujemy:
dofac( { n : = n0} )= C1 , 5( { n : = n0} )= c≤+ { C2)( { n : = n0} )do4( { n : = n0} ), n0≤ 1, jeszcze= c≤+ { cpowrótdopowrót+ c∗+ cpołączenie+ C.fac( { n : = n0- 1 } ), n0≤ 1, jeszcze
Pamiętaj, że pomijamy stan globalny, ponieważ fac
wyraźnie nie ma dostępu do żadnego. Ten konkretny nawrót jest łatwy do rozwiązania
dofac( ψ ) = ψ ( n ) ⋅ ( c≤+ cpowrót) + ( ψ ( n ) - 1 ) ⋅ ( c∗+ cpołączenie)
Omówiliśmy funkcje językowe, które napotkasz w typowym pseudo-kodzie. Uważaj na ukryte koszty podczas analizy pseudo kodu wysokiego poziomu; w razie wątpliwości rozwiń się. Notacja może wydawać się nieporęczna iz pewnością jest kwestią gustu; wymienionych pojęć nie można jednak zignorować. Jednak z pewnym doświadczeniem będziesz mógł od razu zobaczyć, które części stanu są istotne dla której miary kosztów, na przykład „rozmiar problemu” lub „liczba wierzchołków”. Resztę można upuścić - to znacznie upraszcza sprawę!
Jeśli uważasz, że teraz jest to zbyt skomplikowane, informujemy: to jest ! Wyprowadzenie dokładnych kosztów algorytmów w dowolnym modelu, który jest tak blisko rzeczywistych maszyn, że umożliwia przewidywanie czasu wykonywania (nawet względnego), jest trudnym przedsięwzięciem. I to nawet nie bierze pod uwagę buforowania i innych nieprzyjemnych efektów na prawdziwych maszynach.
Dlatego analiza algorytmów jest często upraszczana do tego stopnia, że jest matematycznie wykonalna. Na przykład, jeśli nie potrzebujesz dokładnych kosztów, możesz przeszacować lub niedocenić w dowolnym momencie (dla górnej lub dolnej granicy): zmniejszyć zestaw stałych, pozbyć się warunków warunkowych, uprościć sumy i tak dalej.
Uwaga na temat kosztów asymptotycznych
n
Jest to (często) uczciwe, ponieważ abstrakcyjne stwierdzenia mają pewne (generalnie nieznane) koszty w rzeczywistości, w zależności od maszyny, systemu operacyjnego i innych czynników, a krótkie czasy działania mogą być zdominowane przez system operacyjny ustawiający proces przede wszystkim. W każdym razie masz jakieś zaburzenia.
Oto jak analiza asymptotyczna odnosi się do tego podejścia.
Zidentyfikuj dominujące operacje (które powodują koszty), to znaczy operacje, które występują najczęściej (aż do stałych czynników). W przykładzie Bubblesort możliwym wyborem jest porównanie w linii 5.
Alternatywnie, powiąż wszystkie stałe operacji elementarnych ich maksymalną (z góry) wzgl. ich minimum (od dołu) i wykonaj zwykłą analizę.
- Przeprowadź analizę, korzystając z liczenia wykonania tej operacji jako kosztu.
- OΩ
O
Dalsza lektura
Istnieje wiele innych wyzwań i sztuczek w analizie algorytmów. Oto kilka zalecanych lektur.
Istnieje wiele pytań oznaczonych analizą algorytmów, które wykorzystują techniki podobne do tego.