cecha podzielności
Transkrypt
cecha podzielności
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) Wprowadzenie Rekurencja, zwana również rekursją to odwoływanie się funkcji do samej siebie: opiera się na założeniu istnienia pewnego stanu początkowego wymaga istnienia zdania (lub zdań) stanowiącego podstawę wnioskowania jej istotą jest tożsamość dziedziny i przeciwdziedziny reguły wnioskowania → wynik wnioskowania może podlegać tej samej regule zastosowanej ponownie Silnia Silnia liczby naturalnej n to iloczyn wszystkich liczb naturalnych nie większych niż n. Formalnie definiuje się ją w następujący sposób: n n! = ∏ k, n ≥ 1 k=1 Wartość 0! określa się osobno: 0! = 1 Zwróćmy uwagę, że powyższa definicja może zostać przepisana w postaci rekurencyjnej: n! = { 1, n = 0 n(n − 1)!, n ≥ 1 Implementacja funkcji na podstawie tej definicji jest bardzo prosta: In [1]: def fac(n): if n>=1: return n*fac(n-1) else: return 1 In [2]: fac(0) Out[2]: 1 In [3]: fac(1) Out[3]: 1 In [4]: fac(2) Out[4]: 2 In [5]: fac(5) Out[5]: 120 In [6]: fac(100) Out[6]: 9332621544394415268169923885626670049071596826438162146859296389521 7599993229915608941463976156518286253697920827223758251185210916864 000000000000000000000000 Warto wspomnieć, że w bibliotece math znajdziemy gotową implementację silni: In [7]: import math In [8]: math.factorial(100) Out[8]: 9332621544394415268169923885626670049071596826438162146859296389521 7599993229915608941463976156518286253697920827223758251185210916864 000000000000000000000000 Rekurencja kontra iteracja Niewątpliwą zaletą rekurencji jest przejrzystość programów, które z niej korzystają. Rekurencja jest podstawową 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) Out[10]: 1 In [11]: fac_iter(2) Out[11]: 2 In [12]: fac_iter(5) Out[12]: 120 In [13]: fac_iter(100) Out[13]: 9332621544394415268169923885626670049071596826438162146859296389521 7599993229915608941463976156518286253697920827223758251185210916864 000000000000000000000000 Porównajmy teraz czasy wykonania obu wersji funkcji silnia: In [14]: %%timeit fac(120) 10000 loops, best of 3: 33.1 µs per loop In [15]: %%timeit fac_iter(120) 100000 loops, best of 3: 15 µs per loop Wprawdzie w tym konkretnym przykładzie nie stanowi to dla nas jakiegoś większego problemu, ale ewidentnie metoda rekurencyjna jest dużo wolniejsza od iteracyjnej. Rekurencja potrafi dramatycznie zwiększyć złożoność obliczeniową wykonywanego programu, jeżeli rozwiązywany problem nie ma rekurencyjnego 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) Symbol Newtona Mimo 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: n n! ( ) = k k!(n − k)! Symbol ten pojawia się we wzorze dwumiennym Netwona jako współczynnik w k tym wyrazie rozwinięcia n tej potęgi sumy dwóch składników: n n (x + y) n n−k k = ∑ ( )x y k k=0 Stąd jego druga nazwa: współczynnik dwumienny Newtona. Podana powyżej definicja jest równoważna wzorowi rekurencyjnemu: 1, n ( ) = { n−1 n−1 k ( ) + ( ), k−1 k 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 Out[17]: 21 In [18]: binom(9,3) #84 Out[18]: 84 Sprawdźmy wynik: k ∈ {0, n} 0 < k < n In [19]: fac(9)/(fac(3)*fac(9-3)) Out[19]: 84.0 Cecha podzielności przez 3 dla liczby w zapisie dziesiętnym Cecha podzielności pozwala na stwierdzenie, czy dana liczba jest podzielna bez reszty przez inną bez uciekania 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órej podzielność można określić bardzo prosto, np.: 104628 → 1 + 0 + 4 + 6 + 2 + 8 = 21 → 2 + 1 = 3 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 = 2456 s = str(number) print(s) 2456 Następnie z łańcucha tworzymy listę: In [21]: l = list(s) print(l) ['2', '4', '5', '6'] Listę znaków konwertujemy na listę liczb całkowitych: In [22]: figs = [int(i) for i in l] print(figs) [2, 4, 5, 6] I w ostatnim kroku sumujemy elementy tej listy: In [23]: sum(figs) Out[23]: 17 Korzystając z polecenia map w Pythonie możemy powyższe kroki zapisać jednym poleceniem: In [24]: sum(map(int, str(number))) Out[24]: 17 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) Out[26]: True In [27]: divisible_by_3(4) Out[27]: False In [28]: divisible_by_3(10) Out[28]: False In [29]: divisible_by_3(12) Out[29]: True In [30]: divisible_by_3(104628) Out[30]: True Konwersja liczby całkowitej do łańcucha znaków w dowolnej reprezentacji Załóżmy teraz, że naszym zadaniem jest konwersja liczby całkowitej do łańcucha znaków w dowolnej reprezentacji (od binarnej do szesnastkowej). Dla przykładu możemy chcieć zaprezentować liczbę 10 jako napis "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ów odpowiadają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[32]: '9' Aby przekonwertować większą liczbę, np. 769, musimy ją zatem rozbić najpierw na trzy cyfry a następnie każdą z cyfr zamienić na odpowiedni znak i połączyć znaki ze sobą. Wykorzystamy w tym celu dzielenie całkowite. Zauważmy, że dzieląc całkowicie 769 przez 10, otrzymamy 76 i resztę z dzielenia 9 dzieląc całkowicie 76 przez 10, otrzymamy 7 i resztę z dzielenia 6 dzielą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żemy zamienić 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)) 1453 In [35]: print(toStr(1453,2)) 10110101101 In [36]: print(toStr(1453,8)) 2655 In [37]: print(toStr(1453,16)) 5AD Wielomiany Hermite'a Wielomiany Hermite'a to przykład wielomianów ortogonalnych, używanych między innymi w mechanice kwantowej. Są one rozwiązaniem równania rekurencyjnego: Hn+1 (x) = 2xHn (x) − 2nHn−1 (x) przy warunkach początkowych: H0 (x) = 1 H1 (x) = 2x Kilka pierwszych wielomianów powyższego ciągu ma postać: H2 (x) = 4x H3 (x) = 8x H4 (x) = 16x 4 3 2 − 2 − 12x − 48x 2 + 12 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 = 10 for i in range(0,5): print(hermite(i,x)) 1.0 20 398.0 7880.0 155212.0 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 In [41]: print(h2(x)) print(h3(x)) print(h4(x)) 398 7880 155212 Wieża Hanoi W prezentowanych do tej pory przykładach mieliśmy do czynienia z zagadnieniami, które były zdefiniowane w sposób rekurencyjny. Dlatego zastosowanie rekurecji do ich implementacji było bardzo naturalne. Metoda ta sprawdza się jednak również w bardziej skomplikowanych problemach, które na pierwszy rzut oka nie zawsze wydają się rekurencyjne. Przykładem takiego zagadnienia może być wieża Hanoi, zagadka wymyślona w Azji i sprowadzona do Europy 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. Podczas przekł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ększania parametru wejściowego. Rozwiązanie dla 4 krążków zilustrowane jest na poniższym rysunku: Ogólnie dla n krążków najmniejsza liczba wymaganych ruchów wynosi n L(n) = 2 Dla n − 1 = 64 daje to na przykład 64 2 − 1 = 18446744073709551615 Zakładając, że ręcznie można wykonać 1 ruch na sekundę, przeniesienie wieży zajęłoby 584942417355 lat. Oczywiście komputery wykonują dużo więcej operacji w ciągu sekundy. Chcąc rozwiązać zagadkę na komputerze, zauważmy, że problem da się zapisać w postaci stosunkowo prostego algorytmu rekurencyjnego. Niech n będzie liczbą krążków, natomiast kolejne słupy oznaczone są literami A , B i C . Wówczas: 1. przenieś (rekurencyjnie) n − 1 krążków ze słupka A na słupek B posługując się słupkiem C , 2. przenieś jeden krążek ze słupka A na słupek C , 3. przenieś (rekurencyjnie) n − 1 krążków ze słupka B na słupek C posługując się słupkiem 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") moving disk from A to B moving disk from A to C moving disk from B to C moving disk from A to B moving disk from C to A moving disk from C to B moving disk from A to B In [45]: moveTower(4,"A","B","C") moving disk from A to C moving disk from A to B moving disk from C to B moving disk from A to C moving disk from B to A moving disk from B to C moving disk from A to C moving disk from A to B moving disk from C to B moving disk from C to A moving disk from B to A moving disk from C to B moving disk from A to C moving disk from A to B moving disk from C to B Trójkąt Sierpińskiego Tró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ąta Sierpińskiego, ale zilustrować cały proces na ekranie. W tym celu użyjemy prostego modułu turtle, który udostę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 wn = turtle.Screen() alex = turtle.Turtle() # Allows us to use turtles # Creates a playground for turtles # Create a turtle, assign to alex alex.forward(50) alex.left(90) alex.forward(30) # Tell alex to move forward by 50 units # Tell alex to turn by 90 degrees # 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 turtle wn = turtle.Screen() wn.bgcolor("lightgreen") wn.title("Hello, Tess!") # Set the window background color # Set the window title tess = turtle.Turtle() tess.color("blue") tess.pensize(3) # Tell tess to change her color # 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 [ ]: