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

Podobne dokumenty