WIELKIE LICZBY I PROSTE PROGRAMY Celem niniejszego tekstu

Transkrypt

WIELKIE LICZBY I PROSTE PROGRAMY Celem niniejszego tekstu
WIELKIE LICZBY I PROSTE PROGRAMY
ANDRZEJ KOMISARSKI
Celem niniejszego tekstu jest pokazanie, jak przy pomocy stosunkowo niedużej liczby znaków
można zapisywać bardzo duże liczby. W czasach postępującej komputeryzacji życia codziennego wzrasta również powszechna wiedza o tworzeniu programów i o języków programowania.
Większość uczniów szkół średnich uczy się na lekcjach informatyki już nie tylko podstaw obsługi
komputera, ale często również pisania prostych programów w jednym z popularnych języków
programowania, takich jak Pascal lub C++. Rośnie również zainteresowanie młodzieży tym
tematem. Wychodząc na przeciw temu zjawisku w naszych przykładach będziemy starać się
pokazać, że za pomocą stosunkowo krótkich programów, z wykorzystaniem bardzo prostych instrukcji można uzyskiwać, jako wynik działania programu, zaskakująco (wręcz paradoksalnie)
duże liczby.
Istnieje wiele różnych języków programowania. Zwykle są one bardzo skomplikowane, charakteryzują się dużą liczbą różnych instrukcji i elementów (różnego rodzaju pętle, podprogramy,
funkcje). Dla nas najważniejsza będzie prostota. Dlatego na potrzeby niniejszego tekstu wprowadzimy własny, bardzo uproszczony język programowania.
Program napisany w tym języku ma postać skończonej liczby następujących po sobie linii,
przy czym każda z tych linii zawiera dokładnie jedną instrukcję języka. Każda linia oprócz
instrukcji może zawierać także etykietę. Etykieta to ciąg liter i/lub cyfr, którym może być
poprzedzona instrukcja. Jej zadaniem jest jednoznaczne określenie, nazwanie linii programu.
Jeśli linia zawiera etykietę, to etykieta i właściwa instrukcja są oddzielone dwukropkiem. Może
to wyglądać na przykład tak:
HOP:
b--
HOP jest tu etykietą, zaś b-- instrukcją. Dwie różne linie programu nie mogą być oznaczone tą
samą etykietą. Instrukcje operują na zmiennych. Zmienne noszą nazwy będące ciągami liter
i/lub cyfr. Każda ze zmiennych może przyjmować jedynie wartości całkowite, nieujemne. Na
początku wykonywania programu wszystkie zmienne, których używamy, mają wartość 0.
Dwa spośród trzech rodzajów instrukcji występujących w naszym języku mają wpływ na
wartość zmiennych. Są to instrukcje
oraz
<nazwa zmiennej>++
<nazwa zmiennej>--
na przykład dla zmiennej zm instrukcje te wyglądają następująco:
zm++
oraz
zm--
Pierwsza z tych instrukcji zwięsza wartość zmiennej o 1, druga zaś zmniejsza ją o 1 (o ile zmienna
nie miała już najmniejszej możliwej wartości – czyli 0 – w którym to przypadku zmniejszenie nie
1
2
ANDRZEJ KOMISARSKI
następuje). Dokładniej, jeśli zmienna zm ma wartość n, to po wykonaniu instrukcji zm++ będzie
miała wartość n + 1, zaś po wykonaniu instrukcji zm-- będzie miała wartość max(n − 1, 0).
Wartości pozostałych zmiennych nie ulegają przy tym zmianie.
Trzeci rodzaj instrukcji to instrukcje mające wpływ na kolejność wykonywania instrukcji
programu. Normalnie są one wykonywane jedna po drugiej, w kolejności, w jakiej występują
w programie kolejne linie z tymi instrukcjami. Można to jednak zmienić stosując instrukcję
skoku warunkowego. Instrukcja taka ma postać
<nazwa zmiennej> ?
<etykieta>
<etykieta> to oznaczenie linii do której będzie wykonywany skok. Wykonanie instrukcji skoku
warunkowego nie wpływa na wartości zmiennych, natomiast powoduje, że jeżeli zmienna występująca w instrukcji skoku ma wartość różną od 0 wówczas kolejną wykonywaną instrukcją
będzie instrukcja znajdująca się w linii wskazanej przez etykietę. Jeśli natomiast występująca
w instrukcji skoku zmienna ma wartość 0, wówczas skok nie następuje i w następnej kolejności
wykonana zostanie kolejna linia programu. Ilustruje to prosty przykład:
HOP:
b-...
(kolejne linie programu)
a ? HOP
c++
Wykonanie występującej w tym programie instrukcji a ? HOP powoduje, że następną instrukcją,
która zostanie wykonana zaraz potem jest instrukcja c++ (jeśli a= 0) lub b-- (jeśli a= 0).
Program kończy swe działanie, gdy wykonana zostanie instrukcja znajdująca się w ostatniej
linii programu, a następna instrukcja, która powinna zostać wykonana już nie istnieje.
Będziemy chcieli pisać programy, które będą możliwie krótkie, ale po których zakończeniu
któraś zmienna będzie miała możliwie dużą wartość.
Dla każdej liczby naturalnej n bardzo łatwo jest napisać program, który pozwoli nam uzyskać
liczbę n, jako wartość⎧którejś zmiennej:
⎪
⎪
a++
⎪
⎪
⎪
⎪
⎪
⎨a++
n identycznych linii
⎪
⎪
...
⎪
⎪
⎪
⎪
⎪
⎩a++
Po zakończeniu wykonywania takiego programu zmienna a będzie miała wartość n. Jednak aby
tym sposobem uzyskać liczbę 100, musielibyśmy napisać program mający aż 100 linii.
Spróbujmy napisać program, który pozwoli nam uzyskać liczbę 100 przy użyciu mniej niż
100 linii. Przedstawiony program pozwoli nam przy okazji zobaczyć, w jaki sposób można
wykorzystać instrukcję skoku warunkowego:
WIELKIE LICZBY I PROSTE PROGRAMY
3
Program 1.
1
b++
2
b++
3
b++
4
b++
5
b++
6
b++
7
b++
8
b++
9
b++
16
a++
10
b++
17
a++
11 HOP: b--
18
a++
12
a++
19
a++
13
a++
20
a++
14
a++
21
a++
15
a++
22
b ? HOP
Jak widać, program ma 22 linie. Zostały one ponumerowane, aby ułatwić analizę programu.
Linie od 12 do 21 zwiększają wartość zmiennej a o 10. Fragment programu obejmujący linie
od 11 do 22 działa następująco: Zmienna a zostaje zwiększona o 10, zmienna b zmniejszona
o 1 (o ile nie miała już wartości 0) po czym, jeśli wartość zmiennej b jest dodatnia, wówczas
następuje powrót do linii 11 i cały proces zwiększania zmiennej a i zmniejszania zmiennej b
zostaje powtórzony. W rezultacie, jeśli przed pierwszym wykonaniem instrukcji zawartej w linii
11 zmienna a miała wartość apocz , zaś zmienna b wartość bpocz , to po zakończeniu programu
zmienna b będzie miała wartość 0, zaś zmienna a wartość apocz + 10 max(bpocz , 1) (instrukcje
z linii od 12 do 21 zostaną wykonane raz, jeśli bpocz = 0 lub bpocz razy, jeśli bpocz = 0). Instrukcja
skoku warunkowego posłużyła nam tu do utworzenia pętli obejmującej linie od 12 do 21, przy
czym zmienna b służy jako licznik liczby wykonań tej pętli. Ponieważ podczas pierwszego
wykonania instrukcji znajdującej się w linii 11 zmienna b ma wartość 10, zaś zmienna a wartość
0, więc po zakończeniu wykonaywania programu zmienna a ma wartość 0 + 10 · 10 = 100.
Spróbujmy teraz, zachowując liczbę linii z poprzedniego przykładu, uzyskać większą liczbę
poprzez zwiększenie liczby pętli do dwóch:
Program 2.
1
c++
2
c++
3
c++
4
c++
5
c++
6
c++
7
C: c--
8
b++
9
b++
16
a++
10
b++
17
a++
11
b++
18
a++
12
b++
19
a++
13
b++
20
a++
14
B: b--
21
b ? B
15
a++
22
c ? C
Wykonanie instrukcji z linii od 15 do 20 powoduje zwiększenie zmiennej a o 6. Wykonanie
4
ANDRZEJ KOMISARSKI
fragmentu programu obejmującego linie od 8 do 21 zwiększa zmienną a o 6 · 6 = 36. Po
zakończeniu działania całego programu zmienna a ma wartość 6 · 6 · 6 = 216.
Stosując trzy pętle możemy uzyskać jeszcze więcej:
Program 3.
1
d++
2
d++
3
d++
4
d++
5
D: d--
6
c++
7
c++
8
c++
9
c++
16
a++
10
C: c--
17
a++
11
b++
18
a++
12
b++
19
a++
13
b++
20
b ? B
14
b++
21
c ? C
15
B: b--
22
d ? D
Po zakończeniu wykonywania tego programu zmienna a ma wartość 4 · 4 · 4 · 4 = 256.
Idąc dalej tym tropem można zadać pytanie:
Jak dużą liczbę można uzyskać za pomocą programu mającego 22 linie?
Wydaje się, że bliski odpowiedzi na to pytanie jest rezultat uzyskiwany przez program:
Program 4.
1
e++
2
e++
3
e++
4
E: a--
5
E1: d++
6
9
D: a--
8
D1: c++
16
b ? B
10
C: a--
17
c--
11
C1: b++
18
c ? C1
19
d--
12
a ? E
7
a ? D
a ? C
13
B: b--
20
d ? D1
14
a++
21
e--
15
a++
22
e ? E1
2
2···
Po zakończeniu wykonywania tego programu zmienna a ma wartość 22
− 2, gdzie liczba
dwójek wynosi 65536 (nie licząc dwójki występującej po znaku −). Warto przypomnieć, że
2
2···
w przypadku „piętrowych"potęg takich, jak 22
2
2
(2(2 ) )
=2
(24 )
= 216 = 65536, czyli 2
2
22
22
wykonujemy „od góry", to znaczy 22
=
= ((22 )2 )2 = (42 )2 = 162 = 256.
Zamiast analizować działanie Programu 4, rozważymy jego lekko zmodyfikowaną, łatwiejszą
do prześledzenia wersję:
WIELKIE LICZBY I PROSTE PROGRAMY
5
Program 4’.
1
e++
2
e++
3
e++
4
E: a--
5
d++
6
a ? E
7
D: a--
8
c++
9
a ? D
16
b ? B
10
C: a--
17
c--
11
b++
18
c ? C
12
a ? C
19
d--
13
B: b--
20
d ? D
14
a++
21
e--
15
a++
22
e ? E
Program 4 różni się od Programu 4 jedynie instrukcjami znajdującymi się w liniach 18, 20
i 22 (znajdujące się tam instukcje skoku wskazują inne linie, niż miało to miejsce w przypadku
Programu 4).
Analizowanie Programu 4 zaczniemy od trzech krótkich fragmentów: linii od 4 do 6, linii od
7 do 9 oraz linii od 10 do 12. Fragmenty te stanowią krótkie pętle, podobne do tych, z jakimi
mieliśmy do czynienia we wcześniejszych programach.
Na przykład pętla z linii od 4 do 6 zwiększa wartość zmiennej d o wartość zmiennej a, tą drugą
przy okazji zerując. Jeśli jednak początkowa wartość zmiennej a wynosiła 0, wówczas zmienna
d zostaje zwiększona nie o 0, lecz o 1. W przypadku naszego programu, ilekroć będziemy
rozpoczynali wykonywanie tej pętli, wartość zmiennej d wynosić będzie 0. Tym samym działanie pętli będzie polegało na zamianie wartości zmiennych a oraz d (jeśli początkowa wartość
zmiennej a jest większa od 0) lub na nadawaniu zmiennej d wartość 1 (jeśli początkowa wartość
zmiennej a jest równa 0). Mówiąc prościej, jeśli przed wykonaniem pętli wartość zmiennych a
oraz d wynosiły
a
apocz ,
d
0,
to po jej wykonaniu wartości te będą wynosić
a
0,
d
max(apocz , 1).
Identyczne działanie mają pętle z linii od 7 do 9 oraz linii od 10 do 12. Jedynie zamiast
zmiennej d występują w nich odpowiednio zmienne c oraz b.
Również pętla obejmująca linie od 13 do 16 ma podobne działanie. Jeśli przed jej wykonaniem
zmienne a oraz b miały wartości
a
0,
b
bpocz ,
6
ANDRZEJ KOMISARSKI
to po jej wykonaniu wartości te będą wynosić
a
max(2bpocz , 2),
b
0.
Kluczowe dla zrozumienia działania Programu 4 jest prześledzenie fragmentu programu od
linii 10 do linii 18. Załóżmy, że ten fragment programu zaczyna wykonywać się, gdy zmienne a i
b mają wartość 0, zaś zmienna c ma wartość cpocz . Wykonane zostają po kolei instrukcje z linii
od 10 do 18. Zmienna a ma wartość 2, zmienna b ma wartość 0, zaś zmienna c ma wartość
max(cpocz , 0). Jeśli cpocz jest równe 0 lub 1, wówczas wykonywanie rozważanego fragmentu programu kończy się. Jeśli natomiast cpocz > 1, wówczas instrukcja z linii 18 przenosi wykonywanie
programu ponownie do linii 10. Cała pętla wykonywana jest po raz drugi i będzie wykonana
jeszcze cpocz − 1 razy. Przebieg kolejnych wykonań tej pętli różni się jednak od pierwszego jej
przebiegu. Najpierw (linie od 10 do 12) wartości zmiennych a i b zostają zamienione, a następnie (linie od 13 do 16) wartość zmiennej b zostaje podwojona i ponownie przypisana zmiennej a.
Każde kolejne wykonanie linii od 10 do 16 podwaja zatem wartość zmiennej a. Podsumowując,
jeśli przed wykonaniem rozważanego fragmentu programu zmienne a, b i c miały wartości
a
0,
b
0,
c
cpocz ,
to po jego wykonaniu wartości te będą wynosić
a
max(2cpocz , 2),
b
0,
c
0.
Podobnie, jeśli przed wykonaniem fragmentu programu zawartego w liniach od 7 do 20
zmienne a, b, c i d miały wartości
a
0,
b
0,
c
0,
d
dpocz ,
to po jego wykonaniu wartości te będą wynosić
a
...2
max(22 , 2), gdzie liczba dwójek w piętrowej potędze wynosi dpocz
b
0,
0,
c
d
0.
Wreszcie, po zakończeniu wykonywania całego programu, a więc po trzykrotnym wykonaniu
22
fragmentu programu zawartego w liniach od 4 do 20 zmienna a będzie miała wartość 22
65536. (Istotnie, 2
2
22
można zapisać jako 2
4 = 22 można zapisać jako 2
···2
22
···2
22
=
, gdzie liczba dwójek wynosi 4. Z kolei liczbę
, gdzie liczba dwójek wynosi 2.)
WIELKIE LICZBY I PROSTE PROGRAMY
7
Program 4 różni się od programu 4 tym, że w liniach 18, 20 i 22 występują skoki do linii
5, 8 i 11, nie zaś do poprzedzających je linii 4, 7 oraz 10. Unika się w ten sposób wykonania
znajdujących się w tych liniach instrukcji zmniejszających zmienną a. Czytelnik zechce osobiście
sprawdzić, że zamiast wyniku 65536 uzyskamy dzięki temu zapowiedziany wcześniej rezultat
2
2···
22
− 2, gdzie liczba dwójek wynosi 65536 (nie licząc dwójki występującej po znaku −).
Jak duża jest to liczba? Na pewno jest większa od uzyskanych w Programach 1, 2 i 3 liczb
100, 216 oraz 256. Spróbujmy ją porównać na przykład z ilością atomów we wszechświecie.
Oznaczmy liczbę atomów we wszechświecie literą N. Fizycy podają różne oszacowania wielkości
liczby N. W zależności od źródeł można znaleźć wartości od 1068 do 1081 . Niezależnie od tego,
które z tych oszacowań przyjmiemy za najbliższe prawdzie, zachodzą nierówności:
22
22
2
22
= 65536 < N < 1681 = 2324 < 265536 = 22
.
Oznacza to, że liczba uzyskana za pomocą programu 4 jest znacznie większa od liczby atomów
we wszechświecie, a nawet większa od liczb N!, N N , (N!)!, ((N!)!)!, N N
a nawet N
N
N ···
NN
, ((((((((N!)!)!)!)!)!)!)!)!
, gdzie liczba N występuje 65532 razy. To ostatnie, dosyć zaskakujące, oszaco-
wanie wynika łatwo z następującego twierdzenia, do udowodnienia którego gorąco czytelnika
namawiam:
Twierdzenie. Dla każdej liczby naturalnej n > 1 i liczby naturalnej N zachodzi nierówność
N
N ···
NN
N
<2
2···
N
2(2N)
,
gdzie liczba pięter w potęgach po obu stronach nierówności jest taka sama i wynosi n.
O wielkości liczb, z którymi mamy do czynienia niech zaświadczy na przykład to, że:
• N! to liczba sposobów, na które można ustawić wszystkie atomy wszechświata w jednym
rządku (znaczenie ma kolejność w jakiej te atomy ustawimy).
• Jeśli teraz każdy z tych sposobów opiszemy, każdy na oddzielnym kawałku papieru (pomijam tu oczywistą obserwację, że nie ma tyle papieru, by zapisać choćby jeden sposób
umieszczenia atomów w rządku) to liczba ustawień uzyskanych karteczek w rządku wyN
nosi zaledwie (N!)! co jest liczbą mniejszą niż N N ·N .
• ((((((((N!)!)!)!)!)!)!)!)! to liczba . . . Tego już czytelnikowi oszczędzę.
8
ANDRZEJ KOMISARSKI
Gdybyśmy chcieli oszacować liczbę cyfr w zapisie w systemie dziesiętnym liczby uzyskanej przez
Program 4 musielibyśmy
powiedzieć:
⎧
⎪
⎪
liczba
⎪
⎪
⎪
⎪
⎪
⎪
⎪
liczby
⎪
⎪
⎪
⎪
⎪
⎪
liczby
⎪
⎪
⎨
ponad 65500 linii ...
⎪
⎪
⎪
⎪
⎪
liczby
⎪
⎪
⎪
⎪
⎪
⎪
⎪
liczby
⎪
⎪
⎪
⎪
⎪
⎩
cyfr
cyfr
cyfr
cyfr
cyfr liczby uzyskanej w Programie 4 jest większa
niż liczba atomów we wszechświecie
Przy okazji warto wspomnieć o pewnej funkcji, zwanej funkcją Ackermana. Jej znajomość
ułatwia bowiem śledzenie Programów 4 i 4 . Jest to funkcja dwóch zmiennych. Obie zmienne
mogą przyjmować wartości całkowite, nieujemne. Jest ona określona następująco (w literaturze występuje wiele różnych, nierównoważnych sposobów określania funkcji Ackermana; tutaj
decydujemy się na jeden z nich):
A(n, 0) = n + 2,
A(0, 1) = 0,
A(0, k) = 1 dla k > 1,
A(n + 1, k + 1) = A(A(n, k + 1), k).
Łatwo udowodnić indukcyjnie, że:
A(n, 0) = n + 2,
A(n, 1) = 2n,
A(n, 2) = 2n ,
2
2···
A(n, 3) = 22
(n dwójek).
Śledząc fragment Programu 4 zaczynający się linią 10, a kończący się linią 16 warto sposób,
w jaki zmienia on wartość zmiennej a porównać z funkcji jednej zmiennej A(n, 1) = 2n. Analogicznie, fragmentowi od linii 7 do linii 18 odpowiada funkcja A(n, 2) = 2n , zaś fragmentowi
od linii 4 do linii 20 odpowiada funkcja A(n, 3). Liczba uzyskana wskutek działania całego
programu to A(3, 4) = 65536.
Przy okazji wspomnijmy o innym sposobie zapisywania bardzo dużych liczb, o którym to
sposobie pisał Hugo Steinhaus w „Kalejdoskopie matematycznym":
• Niech n w trójkącie oznacza liczbę nn ,
• niech n w kwadracie oznacza tyle, co n w n trójkątach,
• niech wreszcie n w okręgu oznacza tyle, co n w n kwadratach.
Ile to jest 2 w okręgu? Ile to jest 10 w okręgu?