wstęp do programowania - prac.im.pwr.edu.pl

16
Wstęp do programowania Wykład 8 Podstawowe techniki programowania w przykładach rekurencja Janusz Szwabiński Plan wykładu: Wprowadzenie Silnia Rekurencja kontra iteracja Symbol Newtona Cecha podzielności przez 3 dla liczby w zapisie dziesiętnym Konwersja liczby całkowitej do łańcucha znaków w dowolnej bazie Wielomiany Hermite'a Wieża Hanoi Tró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)

Upload: others

Post on 04-Nov-2021

0 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Wstęp do programowania - prac.im.pwr.edu.pl

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)

Page 2: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 3: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 4: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 5: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 6: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 7: Wstęp do programowania - prac.im.pwr.edu.pl

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]

Page 8: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 9: Wstęp do programowania - prac.im.pwr.edu.pl

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'

Page 10: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 11: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 12: Wstęp do programowania - prac.im.pwr.edu.pl

In [41]:

print(h2(x))print(h3(x))print(h4(x))

3987880155212

Page 13: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 14: Wstęp do programowania - prac.im.pwr.edu.pl

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

Page 15: Wstęp do programowania - prac.im.pwr.edu.pl

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 1­3 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:

Page 16: Wstęp do programowania - prac.im.pwr.edu.pl

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 [ ]: