Czy istnieje magia analizy algorytmów?


159

Istnieje wiele pytań dotyczących sposobu analizowania czasu pracy algorytmów (patrz np i ). Wiele z nich jest podobnych, na przykład ci, którzy proszą o analizę kosztów zagnieżdżonych pętli lub algorytmy dzielenia i zdobywania, ale większość odpowiedzi wydaje się być dostosowana do indywidualnych potrzeb.

Z drugiej strony odpowiedzi na inne ogólne pytanie wyjaśniają szerszy obraz (w szczególności w odniesieniu do analizy asymptotycznej) kilkoma przykładami, ale nie pokazują, jak zabrudzić sobie ręce.

Czy istnieje ustrukturyzowana, ogólna metoda analizy kosztów algorytmów? Kosztem może być czas działania (złożoność czasu) lub inna miara kosztów, taka jak liczba wykonanych porównań, złożoność przestrzeni lub coś innego.

To ma stać się pytaniem referencyjnym, które można wykorzystać do wskazania początkującym; stąd jego szerszy niż zwykle zakres. Prosimy o podanie ogólnych, dydaktycznie przedstawionych odpowiedzi, które są zilustrowane co najmniej jednym przykładem, ale mimo to obejmują wiele sytuacji. Dzięki!


3
Podziękowania dla autora StackEdit za ułatwienie pisania tak długich postów, a moim czytelnikom wersji beta FrankW , Juho , Gilles i Sebastianowi za pomoc w wyeliminowaniu wielu wad wcześniejszych wersji roboczych.
Raphael

1
Hej @Raphael, to cudowne rzeczy. Pomyślałem, że sugeruję poskładanie go jako pliku PDF do rozpowszechnienia? Tego rodzaju rzeczy mogą stać się naprawdę przydatnym odniesieniem.
zjadł

1
@hadsed: Dzięki, cieszę się, że ci się przydał! Na razie wolę, aby link do tego postu był rozpowszechniany. Treści użytkowników SE są jednak „licencjonowane na licencji CC by-sa 3.0 z wymaganą atrybucją” (patrz stopka strony), aby każdy mógł z nich utworzyć plik PDF, pod warunkiem podania informacji o autorze.
Raphael

2
Nie jestem w tym szczególnie kompetentny, ale czy to normalne, że w żadnym pytaniu nie ma odniesienia do twierdzenia Master ?
babou

1
@babou Nie wiem, co tutaj oznacza „normalny”. Z mojego punktu widzenia twierdzenie Master nie ma tu żadnego interesu: chodzi o analizę algorytmów, twierdzenie Master jest bardzo specyficznym narzędziem do rozwiązywania (niektórych) nawrotów (i to w przybliżeniu). Ponieważ matematyka została omówiona gdzie indziej (np. Tutaj ), postanowiłem objąć tutaj tylko część od algorytmu do matematyki. W mojej odpowiedzi podam odniesienia do postów dotyczących pracy z matematyką.
Raphael

Odpowiedzi:


134

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-ja-2)1==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,jotijdoja,jot

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(ZA)=ja=0n-2)jot=0n-ja-2)do5,9(ZA(ja,jot))

oznacza stan tablicy przed ( i , j ) -tą iteracją P 5 , 9 .ZA(ja,jot)(ja,jot)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(ZA(ja,jot))=do5(ZA(ja,jot))+{1,ZA(ja,jot)[jot]>ZA(ja,jot)[jot+1]0,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.

  1. Najgorszy przypadek

    Wystarczy spojrzeć na sumę i zauważyć, że , możemy znaleźć trywialną górną granicę kosztów:do5,9(ZA(ja,jot)){0,1}

    .dozamiana(ZA)ja=0n-2)jot=0n-ja-2)1=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

  2. Najlepszy przypadek

    I odwrotnie, istnieje trywialna dolna granica:

    .dozamiana(ZA)ja=0n-2)jot=0n-ja-2)0=0

    Może się to również zdarzyć: w tablicy, która jest już posortowana, Bubblesort nie wykonuje pojedynczej zamiany.

  3. 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²:

    mi[dozamiana]=1n!ZAja=0n-2)jot=0n-ja-2)do5,9(ZA(ja,jot))

    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(ZA)ZA

    .mi[dozamiana]=1n!ZAinv(ZA)

    Na szczęście dla nas ustalono średnią liczbę inwersji

    mi[dozamiana]=12)(n2))

    co jest naszym końcowym wynikiem. Należy pamiętać, że jest to dokładnie połowa najgorszych kosztów.


  1. Zauważ, że algorytm został starannie sformułowany, aby i = n-1nie została wykonana „ostatnia” iteracja z zewnętrzną pętlą, która nigdy nic nie robi.
  2. ” jest notacją matematyczną dla „wartości oczekiwanej”, która tutaj jest tylko średnią.mi
  3. 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ψψ/P.P

  • 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 Eformularza E1 ∘ E2(powiedzmy, wyrażenie arytmetyczne, gdzie może być dodawanie lub mnożenie, sumujesz koszty rekurencyjnie:

    .domi(ψ)=do+domi1(ψ)+domi2)(ψ)

    Zauważ, że

    • koszt operacji może nie być stały, ale zależy od wartości E 1 i E 2 orazdomi1mi2)
    • 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 Pjako sekwencję programów Q;R, dodajesz koszty do

    .doP.(ψ)=doQ(ψ)+doR(ψ/Q)

  • Warunkowe

    Biorąc pod uwagę program Pformularza if A then Q else R end, koszty zależą od stanu:

    doP.(ψ)=doZA(ψ)+{doQ(ψ/ZA),ZA ocenia na true under ψdoR(ψ/ZA),jeszcze

    Ogólnie rzecz biorąc, ocena Amoże bardzo dobrze zmienić stan, stąd aktualizacja kosztów poszczególnych oddziałów.

  • For-Loops

    Biorąc pod uwagę program Pformularza for x = [x1, ..., xk] do Q end, przypisz koszty

    doP.(ψ)=doinit_for+ja=1kdostep_for+doQ(ψja{x: =xja})

    gdzie jest stanem przed przetwarzaniem wartości , tj. po iteracji z ustawieniem na ..., .ψjaQxixx1xi-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 ximoż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 Pformularza while A do Q end, przypisz koszty

    doP.(ψ) =doZA(ψ)+{0,ZA ocenia na false poniżej ψdoQ(ψ/ZA)+doP.(ψ/ZA;Q), 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({ja: =ja0;x: =x0}) =do<+{0,x00do+=+do/+do1,4({ja: =ja0+1;x: =x0/2)}), jeszcze

    doix

    do1,4i

    do1,4(x)={do>,x0do>+do+=+do/+do1,4(x/2)), jeszcze

    To rozwiązuje z podstawowych środków do

    do1,4(ψ)=log2)ψ(x)(do>+do+=+do/)+do>

    ψ={,x: =5,}ψ(x)=5

  • Wywołania procedur

    Biorąc pod uwagę program Pformularza M(x)dla niektórych parametrów, xgdzie Mjest procedura z (nazwanym) parametrem p, przypisz koszty

    doP.(ψ)=dopołączenie+doM.(ψ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 Motrzymujemy nowy stan lokalny, inicjowany przez ustawienie wartości pna x. Ponadto xmoż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})=do1,5({n: =n0})=do+{do2)({n: =n0}),n01do4({n: =n0}), jeszcze=do+{dopowrót,n01dopowrót+do+dopołączenie+dofac({n: =n0-1}), jeszcze

    Pamiętaj, że pomijamy stan globalny, ponieważ facwyraźnie nie ma dostępu do żadnego. Ten konkretny nawrót jest łatwy do rozwiązania

    dofac(ψ)=ψ(n)(do+dopowrót)+(ψ(n)-1)(do+dopołą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.

  1. 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ę.

  2. Przeprowadź analizę, korzystając z liczenia wykonania tej operacji jako kosztu.
  3. OΩ

O

Dalsza lektura

Istnieje wiele innych wyzwań i sztuczek w analizie algorytmów. Oto kilka zalecanych lektur.

Istnieje wiele pytań oznaczonych które wykorzystują techniki podobne do tego.


1
może jakieś odniesienia i przykłady do głównego twierdzenia (i jego rozszerzeń ) do analizy asymptotycznej
Nikos M.,

@NikosM Tutaj jest poza zakresem (patrz także komentarze do powyższego pytania). Zauważ, że odsyłam do naszego artykułu referencyjnego na temat rozwiązywania nawrotów, który przedstawia twierdzenie Master i in.
Raphael

@Nikos M: moje 0,02 $: podczas gdy główne twierdzenie działa dla kilku powtórzeń, nie będzie dla wielu innych; istnieją standardowe metody rozwiązywania nawrotów. Są też algorytmy, dla których nawet nie będziemy mieli powtarzalności, dając czas działania; konieczne mogą być niektóre zaawansowane techniki liczenia. Dla kogoś z dobrym doświadczeniem matematycznym proponuję znakomitą książkę Sedgewicka i Flajoleta „Analiza algorytmów”, która zawiera takie rozdziały jak „relacje rekurencyjne”, „funkcje generujące” i „asymptotyczne przybliżenia”. Struktury danych pojawiają się jako okazjonalne przykłady, a nacisk kładziony jest na metody!
Jay

@Raphael Nie mogę znaleźć w Internecie żadnej wzmianki o tej metodzie „Tłumaczenie kodu na matematykę” opartej na semantyce operacyjnej. Czy możesz podać jakieś odniesienie do książki, artykułu lub artykułu, które zajmują się tym bardziej formalnie ?. Czy w przypadku, gdy zostało to opracowane przez Ciebie, masz coś głębszego?
Wyvern666,

1
@ Wyvern666 Niestety nie. Sam to wymyśliłem, o ile ktokolwiek może twierdzić, że wymyślił coś takiego. Może w pewnym momencie sam napiszę cytowaną pracę. To powiedziawszy, cały korpus wokół analitycznej kombinatoryki (Flajolet, Sedgewick i wiele innych) jest tego podstawą. Przez większość czasu nie zawracają sobie głowy formalną semantyką „kodu”, ale dostarczają matematyki do radzenia sobie z dodatkowymi kosztami „algorytmów” w ogóle. Szczerze mówiąc, wydaje mi się, że przedstawione tu pojęcia nie są bardzo głębokie - ale matematyka, do której można się zagłębić.
Raphael

29

Liczba wykonanych wyciągów

Istnieje inna metoda, której mistrz Donald E. Knuth w swojej serii The Art of Computer Programming . W przeciwieństwie do tłumaczenia całego algorytmu na jedną formułę , działa on niezależnie od semantyki kodu po stronie „zestawiania rzeczy” i pozwala zejść na niższy poziom tylko wtedy, gdy jest to konieczne, zaczynając od widoku „orła oka”. Każde stwierdzenie może być analizowane niezależnie od reszty, co prowadzi do bardziej przejrzystych obliczeń. Jednak technika ta dobrze nadaje się do dość szczegółowego kodu, nie tyle pseudo kodu wyższego poziomu.

Metoda

Zasadniczo jest to dość proste:

  1. Przypisz każdemu zestawieniu nazwę / numer.
  2. S.jadoja
  3. S.jamija
  4. Oblicz całkowite koszty

    do=jamijadoja

Możesz wstawić oszacowania i / lub wielkości symboliczne w dowolnym punkcie, osłabiając odpowiednio. odpowiednio uogólniając wynik.

mi77O(nlogn)

Przykład: wyszukiwanie w pierwszej kolejności

Rozważ następujący algorytm przechodzenia przez wykres:

dfs(G, s) do
  // assert G.nodes contains s
  visited = new Array[G.nodes.size]     1
  dfs_h(G, s, visited)                  2
end 

dfs_h(G, s, visited) do
  foo(s)                                3
  visited[s] = true                     4

  v = G.neighbours(s)                   5
  while ( v != nil ) do                 6
    if ( !visited[v] ) then             7
      dfs_h(G, v, visited)              8
    end
    v = v.next                          9
  end
end

{0,,n-1}m

ZAbdomija

ja12)3)456789mijaZAZAbbbb+dodob-1do

mi8=mi3)-1foodfsmi6=mi5+mi7while

ZA=1foob=ndo=2)m

ja12)3)456789mija11nnn2)m+n2)mn-12)m

To prowadzi nas do całkowitych kosztów dokładnie

do(n,m)=(do1+do2)-do8)+ n(do3)+do4+do5+do6+do8)+ 2)m(do6+do7+do9).

doja

ja12)3)456789dojan00110101

i dostać

domem(n,m)=3)n+4m

Dalsza lektura

Zobacz na dole mojej drugiej odpowiedzi .


8

Analiza algorytmów, podobnie jak dowodzenie twierdzeń, jest w dużej mierze sztuką (np. Istnieją proste programy (takie jak problem Collatza) ), których nie umiemy analizować). Możemy przekonwertować problem złożoności algorytmu na matematyczny, as kompleksowo odpowiedział Raphael , ale następnie, aby wyrazić zależność od kosztu algorytmu w zakresie znanych funkcji, musimy:

  1. Użyj technik, które znamy z istniejących analiz, takich jak znajdowanie granic na podstawie zrozumienie nawrotów lub sumę / całki, które możemy obliczyć.
  2. Zmień algorytm na coś, co wiemy, jak analizować.
  3. Wymyśl zupełnie nowe podejście.

1
Chyba nie rozumiem, w jaki sposób dodaje to coś przydatnego i nowego, ponad inne odpowiedzi. Techniki są już opisane w innych odpowiedziach. To wydaje mi się bardziej komentarzem niż odpowiedzią na pytanie.
DW

1
Śmiem twierdzić, że inne odpowiedzi dowodzą, że to nie jest sztuka. Możesz nie być w stanie tego zrobić (tj. Matematyki), a pewna kreatywność (jeśli chodzi o zastosowanie znanej matematyki) może być konieczna, nawet jeśli tak jest, ale dotyczy to każdego zadania. Zakładam, że nie chcemy tutaj tworzyć nowej matematyki. (W rzeczywistości to pytanie lub jego odpowiedzi miały na celu wyjaśnienie całego procesu.)
Raphael

4
@Raphael Ari mówi o wymyśleniu rozpoznawalnej funkcji jako ograniczenia, a nie o „liczbie instrukcji wykonanych przez program” (na co adresowana jest twoja odpowiedź). Ogólny przypadek jest sztuką - nie ma algorytmu, który mógłby wymyślić nietrywialne ograniczenie dla wszystkich algorytmów. Typowym przypadkiem jest jednak zestaw znanych technik (takich jak twierdzenie główne).
Gilles

@Gilles Gdyby wszystko, dla którego nie istnieje żaden algorytm, było sztuką, rzemieślnicy (w szczególności programiści) otrzymaliby gorsze wynagrodzenie.
Raphael

1
@AriTrachlenberg czyni ważną kwestię, istnieje wiele sposobów oceny złożoności czasowej algorytmu. Same definicje notacji O oznaczają podpowiedź lub bezpośrednio określają ich teoretyczny charakter w zależności od autora. „Scenariusz najgorszego przypadku” wyraźnie pozostawia miejsce na domysły i / lub nowe fakty pośród dowolnych rozmówców w sali. Nie wspominając już o samej naturze asymptotycznych szacunków, które są czymś… nieciekawym.
Brian Ogden,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.