Upload
others
View
0
Download
0
Embed Size (px)
Citation preview
Wstęp do programowania
Wykład 8 Podstawowe techniki programowania w przykładach rekurencja
Janusz Szwabiński
Plan wykładu:
WprowadzenieSilniaRekurencja kontra iteracjaSymbol NewtonaCecha podzielności przez 3 dla liczby w zapisie dziesiętnymKonwersja liczby całkowitej do łańcucha znaków w dowolnej bazieWielomiany Hermite'aWieża HanoiTrójkąt Sierpińskiego
Bibliografia:
Problem solving with algorithms and data structures using Python,http://interactivepython.org/runestone/static/pythonds/index.html(http://interactivepython.org/runestone/static/pythonds/index.html)
WprowadzenieRekurencja, zwana również rekursją to odwoływanie się funkcji do samej siebie:
opiera się na założeniu istnienia pewnego stanu początkowegowymaga istnienia zdania (lub zdań) stanowiącego podstawę wnioskowaniajej istotą jest tożsamość dziedziny i przeciwdziedziny reguły wnioskowania wynik wnioskowaniamoże podlegać tej samej regule zastosowanej ponownie
SilniaSilnia liczby naturalnej to iloczyn wszystkich liczb naturalnych nie większych niż . Formalnie definiuje sięją w następujący sposób:
Wartość określa się osobno:
Zwróćmy uwagę, że powyższa definicja może zostać przepisana w postaci rekurencyjnej:
Implementacja funkcji na podstawie tej definicji jest bardzo prosta:
→
n n
n! = k, n ≥ 1∏k=1
n
0!0! = 1
n! = { 1,n(n − 1)!,
n = 0n ≥ 1
In [1]:
def fac(n): if n>=1: return n*fac(n-1) else: return 1
In [2]:
fac(0)
In [3]:
fac(1)
In [4]:
fac(2)
In [5]:
fac(5)
In [6]:
fac(100)
Out[2]:
1
Out[3]:
1
Out[4]:
2
Out[5]:
120
Out[6]:
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Warto wspomnieć, że w bibliotece math znajdziemy gotową implementację silni:
In [7]:
import math
In [8]:
math.factorial(100)
Rekurencja kontra iteracjaNiewątpliwą zaletą rekurencji jest przejrzystość programów, które z niej korzystają. Rekurencja jestpodstawową techniką wykorzystywaną w funkcyjnych językach programowania (np. Haskell, Lisp). Chociażdla pewnych problemów stanowi ona naturalny wybór, powinno stosować się ją z umiarem.
Dla ilustracji rozważmy iteracyjną wersję funkcji silnia:
In [9]:
def fac_iter(n): sil = 1 if n>1: for i in range(2,n+1): sil = sil*i return sil
In [10]:
fac_iter(0)
In [11]:
fac_iter(2)
In [12]:
fac_iter(5)
Out[8]:
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Out[10]:
1
Out[11]:
2
Out[12]:
120
In [13]:
fac_iter(100)
Porównajmy teraz czasy wykonania obu wersji funkcji silnia:
In [14]:
%%timeit fac(120)
In [15]:
%%timeitfac_iter(120)
Wprawdzie w tym konkretnym przykładzie nie stanowi to dla nas jakiegoś większego problemu, aleewidentnie metoda rekurencyjna jest dużo wolniejsza od iteracyjnej. Rekurencja potrafi dramatyczniezwiększyć złożoność obliczeniową wykonywanego programu, jeżeli rozwiązywany problem nie marekurencyjnego charakteru.
Inne wady:
rekurencja zwiększa zapotrzebowanie programu na pamięć operacyjnąkompletnie niezależne rozwiązywanie problemów (niektóre wartości wyliczane są wielokrotnie)
Out[13]:
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
10000 loops, best of 3: 33.1 µs per loop
100000 loops, best of 3: 15 µs per loop
Symbol NewtonaMimo wspomnianych wad stosowanie rekurencji jest czasami kuszące ze względu na dużą przejrzystośćkodu. Poniżej omówionych zostanie kilka przykładów, w których można zastosować rekurencję.
Jednym z takich przykładów jest symbol Newtona:
Symbol ten pojawia się we wzorze dwumiennym Netwona jako współczynnik w tym wyrazie rozwinięcia tej potęgi sumy dwóch składników:
Stąd jego druga nazwa: współczynnik dwumienny Newtona.
Podana powyżej definicja jest równoważna wzorowi rekurencyjnemu:
In [16]:
def binom(n,k): if k==0: return 1 if n==k: return 1 else: return binom(n-1,k-1) + binom(n-1,k)
In [17]:
binom(7,2) #powinno być 21
In [18]:
binom(9,3) #84
Sprawdźmy wynik:
( ) =n
k
n!k!(n − k)!
k n
(x + y = ( ))n ∑k=0
n n
kxn−kyk
( ) = {n
k
1,
( ) + ( ),n−1k−1
n−1k
k ∈ {0, n}
0 < k < n
Out[17]:
21
Out[18]:
84
In [19]:
fac(9)/(fac(3)*fac(9-3))
Cecha podzielności przez 3 dla liczby w zapisie dziesiętnymCecha podzielności pozwala na stwierdzenie, czy dana liczba jest podzielna bez reszty przez inną bezuciekania się do dzielenia. W przypadku podzielności przez 3 cecha ma następującą postać:
liczba jest podzielna przez 3, jeśli suma cyfr tej liczby jest podzielna przez 3
Zauważmy, że regułę tę można stosować rekurencyjnie aż do osiągnięcia liczby jednocyfrowej, którejpodzielność można określić bardzo prosto, np.:
Aby zaimplementować sprawdzanie podzielności przez 3 metodą rekursywną, musimy najpierw umieć rozbićdowolną liczbę na jej cyfry i zsumować je. W tym celu przekształcamy liczbę na łańcuch znaków:
In [20]:
number = 2456s = str(number)print(s)
Następnie z łańcucha tworzymy listę:
In [21]:
l = list(s)print(l)
Listę znaków konwertujemy na listę liczb całkowitych:
In [22]:
figs = [int(i) for i in l]print(figs)
I w ostatnim kroku sumujemy elementy tej listy:
104628 → 1 + 0 + 4 + 6 + 2 + 8 = 21 → 2 + 1 = 3
Out[19]:
84.0
2456
['2', '4', '5', '6']
[2, 4, 5, 6]
In [23]:
sum(figs)
Korzystając z polecenia map w Pythonie możemy powyższe kroki zapisać jednym poleceniem:
In [24]:
sum(map(int, str(number)))
Możemy teraz zaimplementować naszą funkcję:
In [25]:
def divisible_by_3(number): ret = False if number in (3,6,9): ret = True if number > 9: ret = divisible_by_3(sum(map(int, str(number)))) return ret
In [26]:
divisible_by_3(3)
In [27]:
divisible_by_3(4)
In [28]:
divisible_by_3(10)
In [29]:
divisible_by_3(12)
Out[23]:
17
Out[24]:
17
Out[26]:
True
Out[27]:
False
Out[28]:
False
Out[29]:
True
In [30]:
divisible_by_3(104628)
Konwersja liczby całkowitej do łańcucha znaków w dowolnejreprezentacjiZałóżmy teraz, że naszym zadaniem jest konwersja liczby całkowitej do łańcucha znaków w dowolnejreprezentacji (od binarnej do szesnastkowej). Dla przykładu możemy chcieć zaprezentować liczbę 10 jakonapis "10" w reprezentacji dziesiętnej, lub jako "1010" w reprezentacji dwójkowej.
Dla ustalenia uwagi załóżmy, że interesuje nas reprezentacja dziesiętna. Jeśli zdefiniujemy łańcuch znakówodpowiadający wszystkim cyfrom w tej reprezentacji,
In [31]:
convString = "0123456789"
to bardzo łatwo będzie nam przekonwertować dowolną liczbę mniejszą od 10. Jeśli naszą liczbą będzie np.9, to odpowiadający jej znak otrzymamy po prostu jako
In [32]:
convString[9]
Out[30]:
True
Out[32]:
'9'
Aby przekonwertować większą liczbę, np. 769, musimy ją zatem rozbić najpierw na trzy cyfry a następniekażdą z cyfr zamienić na odpowiedni znak i połączyć znaki ze sobą. Wykorzystamy w tym celu dzieleniecałkowite. Zauważmy, że
dzieląc całkowicie 769 przez 10, otrzymamy 76 i resztę z dzielenia 9dzieląc całkowicie 76 przez 10, otrzymamy 7 i resztę z dzielenia 6dzieląc całkowicie 7 przez 10, otrzymamy 0 i resztę z dzielenia 7
Zauważmy, że reszty z dzielenia to są cyfry składające się na rozważaną liczbę. Każdą z nich możemyzamienić na znak jak w powyższym przykładzie.
Rekurencyjna wersja tego algorytmu będzie miała następującą implementację:
In [33]:
def toStr(n,base): convertString = "0123456789ABCDEF" if n < base: return convertString[n] else: return toStr(n//base,base) + convertString[n%base]
In [34]:
print(toStr(1453,10))
In [35]:
print(toStr(1453,2))
In [36]:
print(toStr(1453,8))
1453
10110101101
2655
In [37]:
print(toStr(1453,16))
Wielomiany Hermite'aWielomiany Hermite'a to przykład wielomianów ortogonalnych, używanych między innymi w mechanicekwantowej. Są one rozwiązaniem równania rekurencyjnego:
przy warunkach początkowych:
Kilka pierwszych wielomianów powyższego ciągu ma postać:
Poniżej "naiwna" implementacja:
In [38]:
def hermite(n,x): if(n==0): f = 1e0 elif(n==1): f = 2*x else: f = 2*x*hermite(n-1,x)-2*(n-1)*hermite(n-2,x) return f
In [39]:
x = 10for i in range(0,5): print(hermite(i,x))
In [40]:
def h2(x): return 4*x**2-2
def h3(x): return 8*x**3-12*x
def h4(x): return 16*x**4 - 48*x**2 +12
(x) = 2x (x) − 2n (x)Hn+1 Hn Hn−1
(x) = 1H0(x) = 2xH1
(x) = 4 − 2H2 x2
(x) = 8 − 12xH3 x3
(x) = 16 − 48 + 12H4 x4 x2
5AD
1.020398.07880.0155212.0
In [41]:
print(h2(x))print(h3(x))print(h4(x))
3987880155212
Wieża HanoiW prezentowanych do tej pory przykładach mieliśmy do czynienia z zagadnieniami, które były zdefiniowanew sposób rekurencyjny. Dlatego zastosowanie rekurecji do ich implementacji było bardzo naturalne. Metodata sprawdza się jednak również w bardziej skomplikowanych problemach, które na pierwszy rzut oka niezawsze wydają się rekurencyjne.
Przykładem takiego zagadnienia może być wieża Hanoi, zagadka wymyślona w Azji i sprowadzona doEuropy przez francuskiego matematyka Edouarda Lucasa w 1883 roku.
Rozwiązanie zagadki polega na przeniesieniu wieży z jednego słupa na drugi krążek po krążku. Podczasprzekładania można posługiwać się trzecim słupem (buforem), jednak przy założeniu, że nie wolno kłaśćkrążka o większej średnicy na mniejszy ani przekładać kilku krążków jednocześnie.
Jest to przykład zadania, którego złożoność obliczeniowa wzrasta niezwykle szybko w miarę zwiększaniaparametru wejściowego. Rozwiązanie dla 4 krążków zilustrowane jest na poniższym rysunku:
Ogólnie dla krążków najmniejsza liczba wymaganych ruchów wynosi
Dla daje to na przykład
Zakładając, że ręcznie można wykonać 1 ruch na sekundę, przeniesienie wieży zajęłoby lat.
Oczywiście komputery wykonują dużo więcej operacji w ciągu sekundy. Chcąc rozwiązać zagadkę nakomputerze, zauważmy, że problem da się zapisać w postaci stosunkowo prostego algorytmurekurencyjnego. Niech będzie liczbą krążków, natomiast kolejne słupy oznaczone są literami , i .Wówczas:
1. przenieś (rekurencyjnie) krążków ze słupka na słupek posługując się słupkiem ,2. przenieś jeden krążek ze słupka na słupek ,3. przenieś (rekurencyjnie) krążków ze słupka na słupek posługując się słupkiem .
nL(n) = − 12n
n = 64− 1 = 18446744073709551615264
584942417355
n A B C
n − 1 A B CA C
n − 1 B C A
Przykładowa implementacja w Pythonie mogłaby wyglądać tak:
In [42]:
def moveTower(n,A, C, B): if n >= 1: moveTower(n-1,A,B,C) moveDisk(A,C) moveTower(n-1,B,C,A)
In [43]:
def moveDisk(fp,tp): print("moving disk from",fp,"to",tp)
In [44]:
moveTower(3,"A","B","C")
In [45]:
moveTower(4,"A","B","C")
moving disk from A to Bmoving disk from A to Cmoving disk from B to Cmoving disk from A to Bmoving disk from C to Amoving disk from C to Bmoving disk from A to B
moving disk from A to Cmoving disk from A to Bmoving disk from C to Bmoving disk from A to Cmoving disk from B to Amoving disk from B to Cmoving disk from A to Cmoving disk from A to Bmoving disk from C to Bmoving disk from C to Amoving disk from B to Amoving disk from C to Bmoving disk from A to Cmoving disk from A to Bmoving disk from C to B
Trójkąt SierpińskiegoTrójkąt Sierpińskiego to jeden z najprostszych fraktali (znanych długo przed powstaniem tego pojęcia).Konstrukcja tego zbioru podana była w 1915 przez polskiego matematyka Wacława Sierpińskiego:
1. W trójkącie równobocznym połącz środki boków, dzieląc go na cztery mniejsze trójkąty.2. Usuń środkowy z powstałych trójkątów.3. Powtórz kroki 13 dla pozostałych trójkątów.
Tym razem nie tylko będziemy chcieli zaimplementować rekurencyjną metodę tworzenia trójkątaSierpińskiego, ale zilustrować cały proces na ekranie. W tym celu użyjemy prostego modułu turtle, któryudostępnia narzędzia do rysowania i przesuwania obiektu zwanego żółwiem na ekranie.
Dokumentację do modułu można znaleźć pod adresem https://docs.python.org/3.0/library/turtle.html(https://docs.python.org/3.0/library/turtle.html). Jego użycie jest dość proste:
In [46]:
import turtle # Allows us to use turtleswn = turtle.Screen() # Creates a playground for turtlesalex = turtle.Turtle() # Create a turtle, assign to alex
alex.forward(50) # Tell alex to move forward by 50 unitsalex.left(90) # Tell alex to turn by 90 degreesalex.forward(30) # Complete the second side of a rectangle
wn.exitonclick() # Wait for user to close window
Wiele cech żółwia i planszy, na której się porusza, możemy zmieniać, np.:
In [47]:
import turtlewn = turtle.Screen()wn.bgcolor("lightgreen") # Set the window background colorwn.title("Hello, Tess!") # Set the window title
tess = turtle.Turtle()tess.color("blue") # Tell tess to change her colortess.pensize(3) # Tell tess to set her pen width
tess.forward(50)tess.left(120)tess.forward(50)
wn.exitonclick()
Możemy przejść teraz do implementacji właściwego algorytmu:
In [48]:
import turtle
def drawTriangle(points,color,myTurtle): """ Draw triangle given by points (helper function)""" myTurtle.fillcolor(color) myTurtle.up() myTurtle.goto(points[0][0],points[0][1]) myTurtle.down() myTurtle.begin_fill() myTurtle.goto(points[1][0],points[1][1]) myTurtle.goto(points[2][0],points[2][1]) myTurtle.goto(points[0][0],points[0][1]) myTurtle.end_fill()
def getMid(p1,p2): """Find midpoint of triangle's edge (helper function)""" return ( (p1[0]+p2[0]) / 2, (p1[1] + p2[1]) / 2)
def sierpinski(points,degree,myTurtle): """Generate Sierpinski Triangle with recursion""" colormap = ['blue','red','green','white','yellow','violet','orange'] drawTriangle(points,colormap[degree],myTurtle) if degree > 0: sierpinski([points[0], getMid(points[0], points[1]), getMid(points[0], points[2])], degree-1, myTurtle) sierpinski([points[1], getMid(points[0], points[1]), getMid(points[1], points[2])], degree-1, myTurtle) sierpinski([points[2], getMid(points[2], points[1]), getMid(points[0], points[2])], degree-1, myTurtle)
def main(): myTurtle = turtle.Turtle() myWin = turtle.Screen() myPoints = [[-100,-50],[0,100],[100,-50]] sierpinski(myPoints,4,myTurtle) myWin.exitonclick()
main()
In [ ]: