Rekurencja

Standardowe biblioteki użyte w programie:

import numpy   # numeryka
import math    # funkcje matematyczne
import cmath   # liczby zespolone
import random  # liczby losowe
import time    # pomiar czasu
import statistics as st         # statystyka
import matplotlib.pyplot as plt # grafika

Wieże Hanoi

Na początek klasyczny przykład algorytmu rekurencyjnego z rozdz. 1.1.

Etykiety krążków disc zdefiniowane są jako krotka („A”,”B”,”C”,”D”,…), gdzie potrzebujemy co najmniej tyle etykiet, ile wynosi używana później liczba krążków.

disc=("A","B","C","D","E") # użyjemy co najwyżej pięciu krążków

Poniższa funkcja realizuje algorytm rekurencyjny [1.1] (który tutaj wypisuje tylko kolejne instrukcje postępowania, tj. kroki algorytmu). Program han woła (dwakroć) sam siebie, zawsze z mniejszą o 1 liczbą krążków.

def han(n, init, fin, inter): 
    
# n - liczba krążków
# init   - pręt, na którym są początkowo krążki
# fin    - pręt, na którym mają się na koniec znaleźć krążki
# inter  - pręt pośredni

    if n > 0:   # wykonaj, jeśli są krążki   
        
        han(n-1, init, inter, fin)  # przenieś n-1 krążków z init na inter
        
        print("Przenieś krążek", disc[n-1], "z pręta", init, "na pręt", fin)
                # przenieś krążek n z pręta init na fin (wypisz instrukcję)
            
        han(n-1, inter, fin, init)  # przenieś n-1 krążków z inter na fin
# wykonanie dla trzech krążków
han(3,"1","3","2")
Przenieś krążek A z pręta 1 na pręt 3
Przenieś krążek B z pręta 1 na pręt 2
Przenieś krążek A z pręta 3 na pręt 2
Przenieś krążek C z pręta 1 na pręt 3
Przenieś krążek A z pręta 2 na pręt 1
Przenieś krążek B z pręta 2 na pręt 3
Przenieś krążek A z pręta 1 na pręt 3

Powyżej możemy prześledzić dokładnie „historyjkę” z [Rys. 1.2].

# wykonanie dla czterech krążków 
han(4,"1","2","3")
Przenieś krążek A z pręta 1 na pręt 3
Przenieś krążek B z pręta 1 na pręt 2
Przenieś krążek A z pręta 3 na pręt 2
Przenieś krążek C z pręta 1 na pręt 3
Przenieś krążek A z pręta 2 na pręt 1
Przenieś krążek B z pręta 2 na pręt 3
Przenieś krążek A z pręta 1 na pręt 3
Przenieś krążek D z pręta 1 na pręt 2
Przenieś krążek A z pręta 3 na pręt 2
Przenieś krążek B z pręta 3 na pręt 1
Przenieś krążek A z pręta 2 na pręt 1
Przenieś krążek C z pręta 3 na pręt 2
Przenieś krążek A z pręta 1 na pręt 3
Przenieś krążek B z pręta 1 na pręt 2
Przenieś krążek A z pręta 3 na pręt 2

Poniżej bardziej zaawansowana wersja, gdzie śledzimy kolejne stany (tj. ułożenia krążków) na prętach 1, 2, 3. Stany określone są jako łańcuchy znaków A, B, C, … etykietujących krążki, tak jak w rozdz. 1.1. Liczba kroków step określona jest jako global, ponieważ inicjalizujemy ją poza funkcją.

def han2(n, init, fin, inter): 
    
# n - liczba krążków
# init   - pręt, na którym są początkowo krążki
# fin    - pręt, na którym mają się na koniec znaleźć krążki
# inter  - pręt pośredni

    global step # liczba kroków
    
    if n > 0:   # zrób, jeśli są krążki                     
        han2(n-1, init, inter, fin)  # przenieś n-1 krążków z init na inter
        
        fin.append(init.pop())
        step+=1 # licz kroki
        print('krok',step,":")
        print('1:',''.join(one))
        print('2:',''.join(two))
        print('3:',''.join(three))
       
        han2(n-1, inter, fin, init)  # przenieś n-1 krążków z inter na fin
# Stan początkowy: wszystkie trzy krążki na pręcie "1", 
# od największego C do najmniejszego A
one   = ['C', 'B', 'A']
two   = []
three = []

step=0

print('krok', step, ":")
print('1:', ''.join(one))
print('2:', ''.join(two))
print('3:', ''.join(three))

han2(3, one, three, two)
krok 0 :
1: CBA
2: 
3: 
krok 1 :
1: CB
2: 
3: A
krok 2 :
1: C
2: B
3: A
krok 3 :
1: C
2: BA
3: 
krok 4 :
1: 
2: BA
3: C
krok 5 :
1: A
2: B
3: C
krok 6 :
1: A
2: 
3: CB
krok 7 :
1: 
2: 
3: CBA

Jeśli interesuje nas tylko liczba kroków algorytmu Hanoi, implementujemy rekurencję [1.1] w nastepujacy sposób:

# Ciąg Hanoi
def hanoi(n):
    if n == 1:
        return 1
    else: 
        return 2*hanoi(n-1)+1

Możemy teraz sprawdzić równanie (1.2):

for n in range(1,10): print(n, hanoi(n), 2**n-1)
1 1 1
2 3 3
3 7 7
4 15 15
5 31 31
6 63 63
7 127 127
8 255 255
9 511 511

Pamiętacie mnichów buddyjskich? Przenoszą 64 złote krążki między diamentowymi prętami.

hanoi(64)
18446744073709551615
2**64-1
18446744073709551615
# zakładamy, że jeden ruch trwa 5 sekund i przeliczamy czas na lata

x = (2**64-1)*5/60/60/24/365

# format liczby z wykładnikiem
y = '%.1E' % x
print("czas pracy mnichów:",y,"lat")

w=13.7*10**9 # wiek Wszechświata
print(round(x/w),"razy dłużej od wieku Wszechświata")
czas pracy mnichów: 2.9E+12 lat
213 razy dłużej od wieku Wszechświata

Rekurencja z zadania [1.10] posiada \(n^2\) w miejsce \(1\), co prowadzi do oczywistej modyfikacji:

# ciąg Hanoi, gdzie liczymy przeniesioną masę krążków
# krążek n ma masę n**2
def hanoi_m(n):
    if n == 1:
        return 1
    else: 
        return 2*hanoi_m(n-1)+n**2  # tutaj modyfikacja   
[hanoi_m(n) for n in range(1,11)] # tablica 10 pierwszych wyrazów
[1, 6, 21, 58, 141, 318, 685, 1434, 2949, 5998]

Ciąg Fibonacciego

Ciąg Fibonacciego [1.8] programujemy analogicznie do powyższych przykładów:

def fib(n):
    if n == 1:
        return 1
    elif n==2:
        return 1
    else:
        return fib(n-1)+fib(n-2) # suma dwóch poprzednich wyrazów
fib(9)
34
[fib(i) for i in range(1, 11)]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Kolejne ilorazy \(F_{i+1}/F_i\), zaokrąglone do 6 cyfr znaczących:

[round(fib(i+1)/fib(i),6) for i in range(1,16)]
[1.0,
 2.0,
 1.5,
 1.666667,
 1.6,
 1.625,
 1.615385,
 1.619048,
 1.617647,
 1.618182,
 1.617978,
 1.618056,
 1.618026,
 1.618037,
 1.618033]

Jak wiemy z równ. [1.26], powyższe ilorazy dążą do Złotego Podziału \(\phi\) [1.27]:

phi=(1+math.sqrt(5))/2.
print(phi)
1.618033988749895

Jeśli chcemy utworzyć tablicę kolejnych wyrazów ciągu Fibonacciego, postępujemy następująco:

fi=[]          # pusta tablica
fi.append(1)   # pierwszy wyraz
fi.append(1)   # drugi wyraz 

for i in range(2,20):
    fi.append(fi[i-2]+fi[i-1]) # kolejne wyrazy
    
print(fi)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

Metoda równana charakterystycznego

(dla rekurencji liniowej rzędu 2, rozdz. 1.5)

Zakładamy, że rekurencja dla ciągu \(f_n\) ma postać

\(a f_n + b f_{n-1} + c f_{n-2} = 0\) (przy czym stała a jest różna od 0),

np. dla ciagu Fibonacciego \(a=1, b=-1, c=-1\). Rekurencja rzędu 2 wymaga dwóch warunków „początkowych”, znamy zatem dwa różne wyrazy ciagu, \(n\) i \(m\). Słowo „początkowych” ujęte jest w cudzysłów, bowiem niekoniecznie \(n=1\), \(m=2\), jak w przypadku ciągu Fibonacciego. Wskaźniki \(n\) i \(m \neq n\) mogą być dowolne i wynikają z natury konkretnego problemu.

Poniższe pomocnicze funkcje obliczają wyróżnik równania kwadratowego \(\Delta\) oraz jego pierwiastki:

# delta dla równania kwadratowego
def de(a, b, c):
     return b*b-4*a*c

# pierwiastki równania kwadratowego    
def x1x2(a, b, c):
    d=de(a,b,c)  # wyróżnik
    if d>0:      # dla d>0 pierwiastki są rzeczywiste
        return [(-b+math.sqrt(d))/(2*a),(-b-math.sqrt(d))/(2*a)]
    if d<0:      # dla d<0 pierwiastki są zespolone
        return [(-b+cmath.sqrt(d))/(2*a),(-b-cmath.sqrt(d))/(2*a)] # biblioteka cmath
    if d==0:     # dla d=0 jest jeden podwójny pierwiastek rzeczywisty
        return -b/(2*a)

Oto poszczególne przypadki (symbol j oznacza w bibiotece cmath jednostkę urojoną):

print(de(1,3,1), x1x2(1,3,1))
print(de(1,2,2), x1x2(1,2,2))
print(de(1,2,1), x1x2(1,2,1))
5 [-0.3819660112501051, -2.618033988749895]
-4 [(-1+1j), (-1-1j)]
0 -1.0

Liczby zespolone wprowadzamy używając symbolu j, np. w następujący sposób:

1j**2
(-1+0j)

Algorytm [1.2] programujemy w nastepujący sposób:

def sol_rec(a,b,c,n,an,m,am,i):    
    """
    Rozwiąż rekurencję liniową rzędu 2

    input:
    a, b, c - parametry (a różne od 0)
    n       - wskaźnik jednego znanego wyrazu
    an      -   jego wartość
    m       - wskaźnik drugiego znanego wyrazu (n różne od m)
    am      -   jego wartość
    i       - wskaźnik szukanego wyrazu

    output:
    wartość wyrazu i
    """   
    
    if de(a,b,c) != 0:           # dwa różne pierwiastki (delta różna od 0)
        [x1, x2]=x1x2(a, b, c)
        
        # ogólne rozwiązanie na stałe c1 i c2 
        c1=(an*x2**m - am*x2**n)/(x1**n*x2**m - x1**m*x2**n)
        c2=(-an*x1**m + am*x1**n)/(x1**n*x2**m - x1**m*x2**n)

        return c1*x1**i+c2*x2**i # ogólna postać wyrazu i
    
    else:                        # jeden pierwiastek (delta=0)        
        x=x1x2(a, b, c)
        
        # ogólne rozwiązanie na stałe c1 i c2     
        c1=((am*n)/x**m - (an*m)/x**n)/(n - m)
        c2=(am/x**m - an/x**n)/(m - n)  

        return (c1+c2*i)*x**i    # ogólna postać wyrazu i  

Poniżej sprawdzenie dla ciągu Fibonacciego. Ponieważ wiemy, że wyrazy ciągu są liczbami całkowitymi, w celu uzyskania czytelniejszego wydruku stosujemy funkcje real oraz int.

[int(sol_rec(1,-1,-1,1,1,2,1,i)).real for i in range(1,11)]
[0, 0, 1, 2, 4, 8, 13, 21, 34, 55]

Inne przykłady:

def round_complex(x): # zaokrąglanie liczb zespolonych
    return complex(round(x.real),round(x.imag))
[int(round_complex(sol_rec(1,-1,1,1,1,2,1,i)).real) for i in range(1,11)]
[1, 1, 0, -1, -1, 0, 1, 1, 0, -1]
[round(sol_rec(1,2,1,1,1,2,1,i)) for i in range(1,11)]
[1, 1, -3, 5, -7, 9, -11, 13, -15, 17]
[int(round_complex(sol_rec(1,-3,3,1,1,2,1,i)).real) for i in range(1,17)]
[1, 1, 0, -3, -9, -18, -27, -27, 0, 81, 243, 486, 729, 729, 0, -2187]

Błądzenia przypadkowe

Liczby pseudolosowe

Symulowanie procesów stochastycznych wymaga zastosowania liczb pseudolosowych, wbudowanych we wszystkie zaawansowane języki programowania. Zaczynamy od przykładu kostki do gry (3.1), używając biblioteki random.

# kostka do gry: losowa liczba naturalna w zakresie od 1 do 6 
random.randint(1,6)
6

Rzucamy 600000 razy i tworzymy histogram (por. rys. 3.1):

# zbieramy wyniki kolejnych rzutów w tablicy
t=[random.randint(1,6) for i in range(600000)]

plt.figure(figsize=(3,2),dpi=120)                        # rozmiar grafiki
plt.tick_params(axis='both', which='major', labelsize=7) # rozmiar znaczników osi

plt.hist(t, bins=50, facecolor='green')
plt.show()
../_images/rekurencja_59_0.png

Widzimy, że każde oczko wypada po około 100000, czyli kostka jest dobra (niesfałszowana!).

Ruina gracza

W problemie ruiny gracza (rozdz. 1.7) losowanie odbywa się „sfałszowaną” monetą, gdzie prawdopodobieństwo uzyskania orła wynosi \(p\), a reszki \(q=1-p\). Poniżej tworzymy generator liczb losowych, który zwraca 1 z prawdopodobieństwem \(p\) oraz -1 z prawdopodobieństwem \(q\). Uzyskanie 1 oznacza przejście do stanu posiadania większego o 1, a -1 do stanu posiadania mniejszego o 1 (zob. rys. 1.8).

def coin(p):
# zwróć 1 z prawdopodobieństwem p lub -1 z prawdopodobieństwem q=1-p 

    if random.uniform(0,1)<p: # gdy liczba losowa rozłożona jednorodnie 
                              # w przedziale (0,1) jest mniejsza od p 
                              # (tj. prawdopodobientwo tegoż wynosi p)
        return 1              # zwróć 1
    else:                     # w przeciwnym razie
        return -1             # zwróć -1

Sprawdzamy teraz działanie funkcji coin (analogicznie jak dla kostki do gry powyżej). Parametr weights zmienia wysokość słupków tak, aby odpowiadała ona względnej częstości uzyskania 1 lub -1.

p=0.4      # prawdopodobieństwo orła
nu=100000  # liczba rzutów 
t=[coin(p) for k in range(1,nu)]

plt.figure(figsize=(3,2),dpi=120)                        # rozmiar grafiki
plt.tick_params(axis='both', which='major', labelsize=7) # rozmiar znaczników osi

plt.hist(t, bins=30, facecolor='green',weights=1/nu*numpy.ones_like(t))
plt.show()
../_images/rekurencja_65_0.png

Pełna symulacja ruiny gracza jest nastepująca:

time0 = time.time()   # czas startu do pomiaru czasu wykonania programu

n = 40000       # maksymalna liczba kroków (tj. pojedynczych gier dla jednego gracza)
p = 0.495       # prawdopodobieństwo wygrania w pojedynczej grze (uzyskania orła)
q = 1-p         # prawdopodobieństwo przegrania w pojedynczej grze

players   = 200 # całkowita liczba graczy
players_b = 0   # aktualna liczba graczy, którzy rozbili bank

start = 100     # początkowa liczba monet u każdego z graczy
fin   = 200     # liczba monet u gracza, przy której następuje rozbicie banku

x = list(range(n)) # współrzędne x (nr kroku, tj. kolejnej pojedynczej gry)
    
# ustawienia rysunku
plt.figure(figsize=(20,3))                     # rozmiar
plt.title("Ruina gracza, p=" + str(p),fontsize=24) # tytuł
plt.xlim(0,n-1)                                    # zakresy osi
plt.ylim(0,fin)
plt.xlabel('krok',fontsize=24)                     # etykiety osi
plt.ylabel('stan posiadania',fontsize=24)
plt.tick_params(axis='both', which='major', labelsize=16) # rozmiar znaczników osi


# losowe kolory RGB dla poszczególnych graczy
col = [(random.uniform(0,1),random.uniform(0,1),random.uniform(0,1)) 
        for _ in range(players)]

# czasy gry dla wszystkich, tych co rozbili bank i tych, co się spłukali
ti=[] 
ti_b=[]
ti_0=[]

for k in range(players): # pętla po graczach

    y = numpy.zeros(n)   # inicjalizacja macierze    
    i=0                  # początek gry, krok 0
    y[0]=start           # stan posiadania w kroku 0

    while i < n-1: 
        i+=1                  # kolejny krok     
        y[i]=y[i-1] + coin(p) # posiadanie losowo zwieksza się lub zmniejsza o 1
        if y[i]==0:           # jeśli spłukany po i grach
            ti_0.append(i)    # zapisz liczbę kroków (czas gry)
            break             # wyjdź
        if y[i]==fin:         # jeśli rozbił bank po i grach
            players_b+=1      # zwiększ o 1 liczbę graczy, którzy rozbili bank
            ti_b.append(i)    # zapisz liczbę kroków (czas gry)
            break             # wyjdź
            
    plt.scatter(x, y, s=0.01, label=str(k+1)) # zrób wykres dla gracza k
    ti.append(i)                              # zapisz czas gry

lw= max(ti)         # najdłuższy czas gry

plt.xlim(0,1.1*lw)  # zakres osi x o 10% dłuzszy od najdłuższego czsu gry 
    
# wstawka tekstowa do rysunku
plt.text(x=0.85*lw, y=80, s="spłukanych  "+str(len(ti_0)),fontsize=24, color='red')
plt.text(x=0.85*lw, y=120, s="rozbiło bank "+str(len(ti_b)),fontsize=24, color='blue')

# pomiar czasu umożliwia oszacowanie jak długo potrwają obliczenia przy wydłużeniu danych
# w naszym przypadku rośnie on liniowo z liczbą graczy
time1 = time.time()
print("czas wykonania symulacji:", round(time1 - time0, 1),"s")

plt.show()
czas wykonania symulacji: 25.3 s
../_images/rekurencja_67_1.png

Możemy porównać oszacowane z powyższej symulacji prawdopodobienstwo rozbicia banku (równe stosunkowi liczby graczy, którzy rozbili bank, do wszystkich graczy), z dokładnym wzorem na prawdopodobieństwo [1.45]:

print("oszacowane z symulacji prawd. rozbicia banku =",round(players_b/players,2),
      "\npowinno być (dla b. dużej liczby graczy)",round((1-(q/p)**start)/(1-(q/p)**fin),2))
oszacowane z symulacji prawd. rozbicia banku = 0.1 
powinno być (dla b. dużej liczby graczy) 0.12

Podobnie, oszacowany średni czas gry porównyjemy do wzoru (1.48):

print("średni czas gry:",round(st.mean(ti),1),", powinno być (dla b. dużej liczby graczy) ",
      round((-fin*(q/p)**start+start*(q/p)**fin)/(1-(q/p)**fin)/(p-q),1))

print("średni czas gry dla tych, co rozbili bank:",round(st.mean(ti_b),1))

print("średni czas gry dla tych, co się spłukali:",round(st.mean(ti_0),1))
średni czas gry: 7524.3 , powinno być (dla b. dużej liczby graczy)  7429.5
średni czas gry dla tych, co rozbili bank: 8864.4
średni czas gry dla tych, co się spłukali: 7202.4

Widzimy, że średni czas gry dla graczy, którzy rozbili bank, jest dłuższy niż dla tych, co się spłukali (co jest intuicyjne dla \(p<0.5\)).

Ciekawe jest popatrzyć na rozkład czasów gry dla poszczególnych graczy (por. **rys. [1.11]):

plt.figure(figsize=(3,2),dpi=120)                        # rozmiar grafiki

num_bins = 25   # liczba przedziałów histogramu
plt.hist(ti, num_bins, facecolor='blue', alpha=0.5,label="wszyscy")
plt.hist(ti_b, num_bins, facecolor='red', alpha=0.8,label="rozbili bank")
plt.title("Ruina gracza, p="+str(p)+", k="+str(start)+", n="+str(fin),fontsize=9) 
plt.xlabel('czas gry',fontsize=9)
plt.ylabel('liczba graczy',fontsize=9)
plt.tick_params(axis='both', which='major', labelsize=7) # rozmiar znaczników osi
plt.legend(prop={'size':9})                              # legenda
plt.show()
../_images/rekurencja_73_0.png

Silnia jako rekurencja

Obliczamy rekurencyjnie silnię:

def fact(n):
    if n == 0:
        return 1
    else:
        return n*fact(n-1)
fact(7)
5040
# biblioteki matematyczne Pythona mają wbudowaną silnię, np.
math.factorial(7)
5040

Sprawdzimy teraz, jak działa wzór Stirlinga z poprawką [2.9] (bierzemy tylko jeden człon poprawki \(1/12n\)):

def stirling(n):
    return math.sqrt(2*math.pi*n)*(n/math.e)**n*(1+1/(12*n))
for n in range(1,20):
    print(n,math.factorial(n),
          round((stirling(n)-math.factorial(n))/math.factorial(n)*100,3),"%")
1 1 -0.102 %
2 2 -0.052 %
3 6 -0.028 %
4 24 -0.017 %
5 120 -0.012 %
6 720 -0.008 %
7 5040 -0.006 %
8 40320 -0.005 %
9 362880 -0.004 %
10 3628800 -0.003 %
11 39916800 -0.003 %
12 479001600 -0.002 %
13 6227020800 -0.002 %
14 87178291200 -0.002 %
15 1307674368000 -0.001 %
16 20922789888000 -0.001 %
17 355687428096000 -0.001 %
18 6402373705728000 -0.001 %
19 121645100408832000 -0.001 %

Widzimy, że przybliżenie działa bardzo dokładnie, do ułamka procenta, nawet dla małych \(n\). Uwzględnienie kolejnych członów we wzorze [2.9] jeszcze bardziej ulepsza wynik.

Rekurencja w dwóch wymiarach

Symbol Newtona

Skonstruujemy trójkąt Pascala, rys. 2.7 (pamietamy, że „+” łączy listy):

newton=[] # inicjalizacja pustej tablicy

# dodanie [1] jako elementu 0 tablicy
newton.append([1])

# zrobimy 12 wierszy
for n in range(1,12):
    # wiemy, że po bokach są jedynki, stąd [1]+  i   +[1]
    # w środku pętla po k daje (n-1) elementów 
    # każdy jest sumą elemntów z wiersza wyżej w pozycji k-1 oraz k  
    newton.append([1]+[newton[n-1][k-1]+newton[n-1][k] for k in range(1,n)]+[1])

Tablica newton jest dwuwymiarowa:

newton
[[1],
 [1, 1],
 [1, 2, 1],
 [1, 3, 3, 1],
 [1, 4, 6, 4, 1],
 [1, 5, 10, 10, 5, 1],
 [1, 6, 15, 20, 15, 6, 1],
 [1, 7, 21, 35, 35, 21, 7, 1],
 [1, 8, 28, 56, 70, 56, 28, 8, 1],
 [1, 9, 36, 84, 126, 126, 84, 36, 9, 1],
 [1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1],
 [1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1]]

Ładniejszy wydruk uzyskujemy poprzez sformatowane wypisywanie:

for n in range(12):
    row_format ="{:>4}" * (n+1)
    print((12-n)*"  ",row_format.format(*newton[n]))
                            1
                          1   1
                        1   2   1
                      1   3   3   1
                    1   4   6   4   1
                  1   5  10  10   5   1
                1   6  15  20  15   6   1
              1   7  21  35  35  21   7   1
            1   8  28  56  70  56  28   8   1
          1   9  36  84 126 126  84  36   9   1
        1  10  45 120 210 252 210 120  45  10   1
      1  11  55 165 330 462 462 330 165  55  11   1

Sprawdzamy, że suma elementów w wierszu trójkąta Pascala daje \(2^n\), zgodnie ze wzorem [2.14]:

for n in range(12):
    print(sum(newton[n]),2**n)
1 1
2 2
4 4
8 8
16 16
32 32
64 64
128 128
256 256
512 512
1024 1024
2048 2048

Podobnie, suma elementów w wierszu trójkąta Pascala mnożonych naprzemiennie przez \(-1\) daje 0, zgodnie ze wzorem [2.15]:

for n in range(12):
    s=0
    for k in range(n+1):
        s=s+newton[n][k]*(-1)**k
    print(s)
1
0
0
0
0
0
0
0
0
0
0
0

A teraz liczby Fibonacciego, ukryte w trójkacie Pascala wg rys. [2.12]:

for n in range(12):
    s=0
    for k in range(n//2+1):
        s=s+newton[n-k][k]
    print(s)
1
1
2
3
5
8
13
21
34
55
89
144

Liczby Stirlinga I rodzaju (cyklowe)

Wzory wzięte są z tw. 4.1. Procedura jest programistycznie analogiczna do trójkąta Pascala, jedynie po lewej stronie mamy [0] i rekurencja ma inną postać:

stir_c=[]
stir_c.append([1])

for n in range(1,12):
    stir_c.append([0]+[stir_c[n-1][k-1]+(n-1)*stir_c[n-1][k] for k in range(1,n)]+[1])
for n in range(9):
    row_format ="{:>6}" * (n+1)
    print(row_format.format(*stir_c[n]))
     1
     0     1
     0     1     1
     0     2     3     1
     0     6    11     6     1
     0    24    50    35    10     1
     0   120   274   225    85    15     1
     0   720  1764  1624   735   175    21     1
     0  5040 13068 13132  6769  1960   322    28     1

Suma elementów w wierszu daje n! - liczbę wszystkich permutacji:

for n in range(0,12):
    print(sum(stir_c[n]),math.factorial(n))
1 1
1 1
2 2
6 6
24 24
120 120
720 720
5040 5040
40320 40320
362880 362880
3628800 3628800
39916800 39916800

Suma elementów w wierszu mnożonych naprzemiennie przez \(-1\) daje 0, począwszy od \(n=2\)

for n in range(12):
    s=0
    for k in range(n+1):
        s=s+stir_c[n][k]*(-1)**k
    print(s)
1
-1
0
0
0
0
0
0
0
0
0
0

Liczby Stirlinga II rodzaju (podziałowe)

Teraz bierzemy wzory z tw. 4.2:

stir_p=[]
stir_p.append([1])

for n in range(1,12):
    stir_p.append([0]+[stir_p[n-1][k-1]+k*stir_p[n-1][k] for k in range(1,n)]+[1])
for n in range(10):
    row_format ="{:>5}" * (n+1)
    print(row_format.format(*stir_p[n]))
    1
    0    1
    0    1    1
    0    1    3    1
    0    1    7    6    1
    0    1   15   25   10    1
    0    1   31   90   65   15    1
    0    1   63  301  350  140   21    1
    0    1  127  966 1701 1050  266   28    1
    0    1  255 3025 7770 6951 2646  462   36    1

Sumy w poszczególnych wierszach tworzą liczby Bella, zgodnie ze wzorem [4.6]:

for n in range(12):
    print(n,sum(stir_p[n]))
0 1
1 1
2 2
3 5
4 15
5 52
6 203
7 877
8 4140
9 21147
10 115975
11 678570

Zadania

Wszystkie zadania dotyczą programowania w Pythonie. Zadania „teoretyczne” znajduja się w podstawowej części książki.

  1. Rozwiaż zad. [1.1] z pomocą funkcji sol_rec.

  2. Utwórz funkcję realizującą rekurencję pierwszego rzędu z członem niejednorodnym postaci \(a+b n\) i rozwiąż numerycznie problem dzielenia pizzy z zad. [1.2]).

  3. Wygeneruj liczby Fibonacciego i sprawdź numerycznie wszystkie przypadki z zad. [1.5] oraz [1.6].

  4. Utwórz funkcję realizującą rekurencję z zad. [1.12].

  5. Zmodyfikuj kod dla Ruiny gracza z wykładu, aby rozwiązać zad. [1.21], [1.22] i [1.23].

  6. Oblicz i narysuj trójkąt liczb Bella z rys. [4.5].

  7. Oblicz i narysuj trójkąt partycji \(p(n,k)\) ze wzoru [4.20], rys. [4.11].

  8. Oblicz i narysuj trójkąt partycji \(q(n,k)\) ze wzoru [4.24], rys. [4.13].

  9. Uruchom program ilustrujący algorytm Kratsuby-Ofmana (rozdz. [1.8]) ze strony www.codeandgadgets.com/karatsuba-multiplication-python/ i przedyskutuj wyniki.