18. Amnésia recursiva¶
Estivemos trabalhando com recursão e continuaremos apreciando alguns dos seus aspectos. Na realidade estaremos a todo momento trabalhando com tudo que vimos até agora.
A perda parcial ou total da memória, a amnésia, é um mal que assola alguns programas. Ocasionalmente seus efeitos são sentidos. Funções recursivas constituem uma área onde a amnésia tem seus sintomas muito amplificados. Portanto, este parece ser um momento apropriado para abordarmos este tópico.
18.1. Objetivos de aprendizagem¶
Cada tópico que estudamos, por mais simples que possa parecer, pode ser aprofundado em diversas direções. O nosso progresso de aprendizagem é meio que em zigue-zague. Neste ponto, por assim dizer, o nosso zigue será a amnésia em programação e, em particular, a amnésia em recursão. O nosso zague será o antídoto para amnésia que serão as variáveis e seu papel na memorização. 😧 Junto com esse zigue-zague veremos alguns rudimentos sobre como fazer a análise da eficiência experimental e analítica de algoritmos.
18.2. Introdução¶
Váriáveis são nomes ou apelidos que damos às coisas, objetos, valores, …, que pretendemos utilizar mais tarde. Algo bem básico, não? Talvez…
Variáveis nos permitem reacessar, rapidamente, objetos que já atravessaram o nosso caminho. Dependendo do número de vezes que necessitamos acessar alguns valores, o uso de nomes, apelidos, rótulos, referências para revisitar valores pode ser a diferença entre realizarmos uma tarefa em um tempo aceitável ou em um tempo que pode ser algo como a idade da Terra!
Confusa?! Confuso?! 😕 Bem vamos por a mão na massa e depois você pode reler estes parágrafos iniciais e refletir se fazem algum sentido. No caso, a nossa mão será um conjunto de variáveis e a nossa massa será o problema de calcular os números de Fibonacci, que tem características intrinsicamente recursivas.
Tenha em mente que os números de Fibonacci são um contexto muito simples utilizado apenas como pretexto para apresentar ideias muito sofisticadas. Neste momento, aplicar ideias absolutamente envolventes a problemas complexos não parece ser algo pedagogicamente promissor 🤔.
18.3. Fibonacci¶
Os número de Fibonacci formam uma sequência de números em que cada número é a soma dos dois números que o antecedem. A sua definição é, portanto, recursiva. O valor do \(n\)-ésimo número \(\mathtt{F_n}\) depende dos valores de \(\mathtt{F_{n-1}}\) e \(\mathtt{F_{n-2}}\).
Como sabemos, toda recursão tem um caso base e com os número de Fibonacci não é diferente. Sem mais delongas, aqui está a especificação recursiva dos números de Fibonacci, com base e tudo mais:
Assim, os primeiros valores da sequência são:
Para escrever uma versão recursiva de uma função fibonacciR()
que calcula e retorna o \(n\)-ésimo número de Fibonacci
basta traduzir a especificação para Python.
def fibonacciR(n):
'''(int) -> int
RECEBE um inteiro não negativo n.
RETORNA o n-ésimo número de Fibonacci.
'''
if n == 0: return 0
if n == 1: return 1
return fibonacciR(n-1) + fibonacciR(n-2)
Escrever uma versão iterativa fibonacciI()
também é uma tarefa simples.
def fibonacciI(n):
'''(int) -> int
RECEBE um inteiro não negativos n.
RETORNA o n-ésimo número de Fibonacci.
'''
if n == 0: return 0
if n == 1: return 1
anterior = 0
atual = 1
for i in range(1, n):
proximo = atual + anterior
anterior = atual
atual = proximo
return atual
Agora vamos ao que interessa!
Qual versão é mais eficiente em termos de tempo?
Para responder essa pergunta faremos alguns experimentos e usaremos a função time.time()
do módulo time
do Python
para cronometrar o tempo de execução por meio da seguinte função.
def cronometro(f, n):
'''(callable, int) -> float, valor
RECEBE uma função f e um argumento n para f().
RETORNA o tempo gasto e valor retornado pela execução de f(n).
'''
# dispare o cronômetro
inicio = time.time()
# execute a função
valor = f(n)
# pare o cronometro
fim = time.time()
# calcule quantos segundos foram gastos.
elapsed = fim-inicio
return elapsed, valor
Execute a função main()
no Trinket ou baixe o arquivo main.py
e fibonacci,.py
para o seu computador.
Ao executar a função main()
no meu computador obtive os seguinte valores para as funções fibonacciR()
e fibonacciI()
:
fibonacciR() fibonacciI()
n f(n) tempo (s) n f(n) tempo (s)
---------------------------- ---------------------------
30 832040 0.39 30 832040 0.00
31 1346269 0.49 31 1346269 0.00
32 2178309 0.77 32 2178309 0.00
33 3524578 1.16 33 3524578 0.00
34 5702887 1.87 34 5702887 0.00
35 9227465 3.02 35 9227465 0.00
36 14930352 4.93 36 14930352 0.00
37 24157817 7.94 37 24157817 0.00
38 39088169 12.85 38 39088169 0.00
39 63245986 20.87 39 63245986 0.00
40 102334155 35.66 40 102334155 0.00
Hmm. 🤔 Será que tem algo errado? Os tempos de fibonacciI()
foram praticamente zero!
A propósito, é claro que os tempos não foram zero. Algum tempo essas execuções gastaram. O ponto é que o tempo gasto foi abaixo da nossa medida padrão.
Bem, voltando. Tudo é meio estranho pois fibonacciR()
é muito simples e é até mais curta que fibonacciI()
.
Gastar mais que meio minuto para calcular fibonacciR(40)
é muita coisa. Precisamos investigar o que está acontecendo. 🕵🏿♀️
Como boas detetives vamos examinar as evidências. Examinaremos os rastros deixados pela execução de fibonacciR()
para alguns valores de n
. 🕵🏻♀️
In [18]: fibonacciR(5)
fibonacciR(5)
fibonacciR(4)
fibonacciR(3)
fibonacciR(2)
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(0)
fibonacciR(0)=0
fibonacciR(2)=1
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(3)=2
fibonacciR(2)
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(0)
fibonacciR(0)=0
fibonacciR(2)=1
fibonacciR(4)=3
fibonacciR(3)
fibonacciR(2)
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(0)
fibonacciR(0)=0
fibonacciR(2)=1
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(3)=2
fibonacciR(5)=5
Out[18]: 5
Hmm. Aqui tem algo estranho. Para calcular \(F_5\) deveria ser suficiente fazermos umas quatro adições e estamos
vendo várias chamadas de fibonacciR(0)
, fibonacciR(1)
e fibonacciR(2)
, duas chamadas de fibonacciR(3)
e uma chamada de fibonacciR(4)
.
É muita coisa para calcular apenas \(F_5\). Vejamos o que ocorre para n = 8. 🕵🏿♀️
In [20]: fibonacciR(8)
fibonacciR(8)
fibonacciR(7)
fibonacciR(6)
fibonacciR(5)
fibonacciR(4)
fibonacciR(3)
fibonacciR(2)
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(0)
fibonacciR(0)=0
[... montes de linhas apagadas ...]
fibonacciR(1)
fibonacciR(1)=1
fibonacciR(0)
fibonacciR(0)=0
fibonacciR(2)=1
fibonacciR(4)=3
fibonacciR(6)=8
fibonacciR(8)=21
Out[20]: 21
Vixe! Nem cabe na página! O rastro tem mais de 130 linhas!
Pelo menos descobrimos o que está ocorrendo. A função fibonacciR()
resolve o mesmo subproblema
várias vezes. Por exemplo, fibonacci(8)
fez 13 chamadas de fibonacci(2)
, que tem valor 1.
Assim o diagnóstico da lentidão da função fica fácil, fibonacciR()
está sofrendo de amnésia! 🤒
Felizmente esse mal tem tratamento.
A nossa receita para fibonacciR()
é um escalda pés de variáveis com treinamento em memorização.
Mais adiante veremos como tratar fibonacciR()
de uma maneira
sistemática, usando protocolos muito bem estabelecidos.
Esse tratamento para amnésia de funções é utilizado na cura de funções que resolvem problemas muito
mais complicados, mas muito mesmo! Antes do tratamento, vamos fazer umas contas para estimar o
trabalho feito por fibonacciR()
com adições.
18.4. Estimativa do trabalho¶
Quantas adições faz a chamada fibonacciR(n)
?
Não deveriam ser muitas, mas parece que são… 😥
Vamos chamar de \(\mathtt{T(n)}\) o número de adições + feitas por fibonacciR(n)
.
É claro que o número de adições cresce a medida que n
cresce.
É de se esperar que o tempo gasto por fibonacciR(n)
seja proporcional a \(\mathtt{T(n)}\) e
que cresça com a mesma velocidade. A tabela abaixo mostra \(F_n\) e \(T(n)\), calculados na mão, para alguns valores de \(n\).
Antes de prosseguir, vamos reescrever fibonacciR()
de uma forma que seja mais conveniente para a nossa análise, inclusive numerando algumas linhas.
def fibonacciR(n)
1 if n == 0: return 0
2 if n == 1: return 1
3 return fibonacciR(n-1) +
4 fibonacciR(n-2)
Examinando o número de adições feitas em cada linha do código temos que
Assim, obtemos a seguinte descrição de \(\mathtt{T(n)}\) que se parece muito com a especificação da função de Fibonacci:
\[\begin{split}\begin{align*} \mathtt{T(n)} &= \begin{cases} 0 & \text{ se $\mathtt{{n} = 0}$},\\ 0 & \text{ se $\mathtt{{n} = 1}$},\\ \mathtt{T(n-1) + T(n-2) + 1} & \text{ se $\mathtt{{n\geq 2}}$}. \end{cases} \end{align*}\end{split}\]
Fazendo algumas contas é possível verificar que para \(\mathtt{n \geq 6}\) vale que \(\mathtt{T(n) \geq (3/2)^n}\).
Em português, o número de adições feitas por fibonacciR(n)
cresce exponencialmente a medida que n
cresce.
Portanto, o tempo gasto por fibonacciR(n)
para fazer o seu serviço deve também crescer muito rapidamente
a medida que n
cresce. Para termos uma ideia, essa estimativa diz que fibonacciR(40)
faz pelo menos
\(\mathtt{T(n) \geq (3/2)^{40}> 11057332}\) adições. Isso é mais do que 11 milhões de adições! 😵💫 🤦🏾
Enquanto isso, na bat caverna, fibonacciI(40)
faz não mais que 40 adições. 🥇
Mais sobre a estimativa para \(\mathtt{T(n)}\)
Para \(\mathtt{n \geq 6}\) vale que \(\mathtt{T(n) \geq (3/2)^n}\), em que \(\mathtt{T(n)}\) é o número de adições feita por fibonacciR(n)
.
Vejamos uma demonstração desse fato.
A demonstração será por indução no número n
.
A base da indução são os casos em que \(\mathtt{n = 6}\) e \(\mathtt{n = 7}\)
Calculando diretamente obtivemos que \(\mathtt{T(6)=12 > 11.40 > (3/2)^6}\) e que \(\mathtt{T(7)=20 > 18 > (3/2)^7}\).
Suponha por hipótese de indução que \(\mathtt{T(k) \geq (3/2)^k}\) para todo \(\mathtt{k}\), \(\mathtt{6 \leq k < n}\) para algum \(\mathtt{n \geq 8}\).
Verifique que essa hipótese implica que \(\mathtt{T(n) \geq (3/2)^n}\).
De fato
Portanto, pelo Princípio da Indução, concluímos que \(\mathtt{T(n) \geq (3/2)^n}\) para todo \(\mathtt{n \geq 6}\).
De maneira curta e grossa, isso, como dizem por ai, significa que o consumo de tempo de FibonacciR()
é exponencial. 👎🏿
De forma mais precisa …
Na verdade é possível calcular exatamente o número \(\mathtt{T(n)}\) de adições feitas por FibonacciR(n)
😲.
Para \(\mathtt{n = 0, 1, 2,\ldots}\) tem-se que
em que
Esse fato pode ser verificado por indução em \(n\) e usando-se o fato de que \(1 + \phi = \phi^2\) e de que \(1 + \hat{\phi} = \hat{\phi}^2\).
Vejamos alguns valores de \(\mathtt{T(n)}\);
Deu para sentir o que significa gastar ou consumir tempo exponencial? 🤦🏿♀️ 🤦🏽♀️ 🤦🏻♀️
18.5. Árvores de recursão¶
Vamos fazer uma pequena pausa e antes de apresentarmos um tratamento para amnésia, vejamos uma outra maneira de
visualizarmos esse comportamento exponencial de fibonacciR()
por meio do que é chamado de uma
árvore de recursão. Curiosamente, em Ciência da Computação, frequentemente, as raízes das árvores são desenhadas para cima
e suas folhas para baixo 😖. As árvores em computação crescem para baixo. 🤔
Lembram árvores genealógicas.
Aqui está a árvore da recursão para determinar \(\mathtt{F_5}\) associada a chamada fibonacciR(5)
.
\(F_5\) é a raiz da árvore.
Nas folhas da árvore temos os valores da base da recursão, \(\mathtt{F_1}\) e \(\mathtt{F_0}\)
Os dois \(\mathtt{F_4}\) e três \(\mathtt{F_2}\) aparecem no que chamamos de nós internos da árvore, aqueles que não são raiz nem folhas.
A árvore da recursão acima tem 6 nós internos, 8 folhas e, evidentemente, uma raíz.
Como em uma árvore genealógica, dizemos que a raiz \(\mathtt{F_5}\) é mãe de \(\mathtt{F_4}\) e de \(\mathtt{F_3}\). Por sua vez, \(\mathtt{F_4}\) é filha \(\mathtt{F_5}\) e mãe de \(\mathtt{F_3}\) e de \(\mathtt{F_2}\). Veja que há um nó \(F_3\) que é filha de \(\mathtt{F_5}\) e outro que é neta. Bem , como você pode imaginar, temos todas as relações familiares na árvore, mães, filhas, netas, bisnetas, tias, bisavó, … Essa nomenclatura será utilizada mais tarde.
Veja que a árvore da recursão para \(\mathtt{F_{60}}\) terá mais de 1 trilhão de nós internos e folhas. A copa dessa árvore será muito bonita mesmo. 😲
Depois desta pausa, vamos ao tratamento da amnésia.
18.6. Tratamento para amnésia¶
A ideia é usar uma tabela para mantermos os resultados já calculados por chamadas anteriores da função. Dessa forma assim que a função é chamada novamente com o mesmo argumento, o valor armazenado é prontamente retornado. Essa técnica para deixar programas mais velozes é chamada de memorização ou Memoization. Frequentemente a tabela utilizada pela técnina é batizada com o nome de cache.
O tratamento para curar amnésia consiste em inicialmente construir uma função envoltória para a função recursiva. Nesta envoltória é criada a memória cache que será usada pela função recursiva como rascunho para anotar o que andou fazendo. Assim, quando a função recursiva necessitar se lembrar de algo basta primeiro consultar suas anotações no rascunho. Se a função não encontrar algum registro em suas anotações saberá que deverá fazer algo para completar o serviço que lhe foi pedido.
No caso de fibonacciR()
, chamaremos essa envoltória de fibonacciRM()
de Fibonacci recursiva com memorização.
def fibonacciRM(n):
'''(int) -> int
envoltória para fibonacciRCache().
'''
# -1 indica que o valor correspondente a posição não foi calculado
cache = [0] + [-1]*n
return fibonacciRCache(n, cache)
A função recursiva é então substituida por uma versão modificada que utiliza os recursos da memória cache.
No caso de fibonacciR()
, a função substituta será fibonacciRCache()
:
def fibonacciRCache(n, cache):
'''(int) -> int
RECEBE um inteiro não negativos n.
RETORNA o n-ésimo número de Fibonacci.
'''
# BASE
# se FinonacciR(n) está no cache, retorne seu valor
if cache[n] > -1: return cache[n]
elif n < 2: cache[n] = n
else:
# REDUZA: se Finonacci(n) não está no cache, calcule-o recursivamente
cache[n-1] = fibonacciRCache(n-1, cache)
# cache[n-2] = fibonacciRCache(n-2, cache) # Supérflua. Por quê?
# RESOLVA: calcule Fibonacci(n)
cache[n] = cache[n-1] + cache[n-2]
return cache[n]
Note a semelhança estrutural de fibonacciRCache()
e fibonacciR()
.
Vejamos agora como a dobradinha fibonacciRM()
e fibonacciRCache()
se comportam nos testes:
fibonacciRM()
n f(n) tempo (s)
---------------------------
30 832040 0.00
31 1346269 0.00
32 2178309 0.00
33 3524578 0.00
34 5702887 0.00
35 9227465 0.00
36 14930352 0.00
37 24157817 0.00
38 39088169 0.00
39 63245986 0.00
40 102334155 0.00
Muito melhor, não?!
Esta nova versão de fibonacciR()
está completamente curada da amnésia.
18.7. Hanoi e o fim do mundo¶
Vimos como amnésia pode causar que uma função simples e elegante gaste uma quantidade de tempo absurda para fazer seu serviço. Vejamos agora uma função que gasta muito, mas muito tempo mesmo para fazer seu serviço, mas a culpa não é dela. Ela tem realmente muito serviço a fazer.
Diz a lenda que \(\ldots\)
No grande templo de Brahma em Benares, na Índia, em uma placa de bronze sob a cúpula que marca o centro do mundo, há 64 discos de ouro puro que os sacerdotes carregam um de cada vez entre três agulhas de diamante de acordo com a lei imutável de Brahma: nenhum disco pode ser colocado em um disco menor. No início do mundo todos os 64 discos formaram a Torre de Brahma em um agulha. Agora, porém, o processo de transferência da torre de uma agulha para outra está no meio do caminho. Quando o último disco for finalmente colocado no seu lugar, mais uma vez formando a Torre de Brahma, mas em uma agulha diferente, então virá o fim do mundo e tudo se transformará em pó.
- W.W. Rouse Ball & H.S.M. Coxeter, Mathematical Recreations and Essays, 12th edition. Univ. of Toronto Press, 1974. The De Parville account of the origen from La Nature, Paris, 1884, part I, pp. 285-286.
A função hanoi()
do capítulo anterior seria de ajuda para os sacerdotes já que apresentam uma
sequência de movimentos para resolver o quebra-cabeça.
def hanoi(n, origem, auxiliar, destino):
'''(int, str, str, str) -> None
Recebe o numero de discos n, os apelidos origem, auxiliar e destino dos pinos.
Imprime as messagens com os movimentos para resolver o quebra-cabeça das Torres de Hanoi
para movimentar n discos do pino origem para o pino destino usando o pino auxiliar.
'''
if n == 0: return None
hanoi(n-1, origem, destino, auxiliar)
print(f"mova o disco {n} do pino {origem} para o pino {destino}")
hanoi(n-1, auxiliar, origem, destino)
A chamada hanoi(3,'A','B','C')
produz as mensagens:
mova o disco 1 do pino A para o pino C
mova o disco 2 do pino A para o pino B
mova o disco 1 do pino C para o pino B
mova o disco 3 do pino A para o pino C
mova o disco 1 do pino B para o pino A
mova o disco 2 do pino B para o pino C
mova o disco 1 do pino A para o pino C
A animação abaixo mostra uma sequência de movimentos que resolve o quebra-cabeça com 3 discos.
A questão que queremos tratar é determinar ou pelo menos estimar
quantos movimentos são feitos por hanoi(n,'A','B','C')
para
solucionar o quebra-cabeça com n
discos.
Se \(\mathtt{T(n)}\) é o número de movimentos feitos pela função
para resolver o quebra-cabeça com n
discos, então desejamos
determinar o valor de \(\mathtt{T(n)}\). Para
aquecer os motores, aqui estão alguns valores de \(\mathtt{T(n)}\)
determinados por uma modificação da função hanoi()
que além de imprimir
os movimentos retorna o número de movimentos feitos.
Vemos pela estratégia usada pela função que, para resolver o quebra-cabeça com n
discos, devemos:
- resolver o quebra-cabeça com
n-1
discos;- fazer o movimento do maior disco para sua posição final; e
- resolver o quebra-cabeça com
n-1
discos;
Desta solução obtemos a seguinte relação recursiva que descreve \(\mathtt{T(n)}\), também chamada de relação de recorrência:
Quanto vale \(\mathtt{T(n)}\)? Desenrolando telescopicamente \(\mathtt{T(n)}\) observamos que
Logo,
Aqui aparece novamente uma função exponencial que não é sinal de boa coisa.
Uma observação mais sutil.
A função hanoi()
faz o número mínimo de movimentos possível para resolver o quebra-cabeça. Não é possível resolver o quebra-cabeça com n
discos fazendo menos do que \(\mathtt{2^n-1}\) movimentos!
Voltemos aos sacerdotes \(\ldots\) Vamos estimar o tempo que eles podem gastar para mover a Torre de Brahma, associando um tempo para realizar cada movimento. O número de movimentos necessários são:
Suponhamos que os monges consigam mover 1 disco por segundo. Parece bem rápido, certo?! Nessas condições, eles conseguiriam mover a torre (completar os movimentos) em
A idade da estimada Terra é de 4,54 bilhões de anos. Ufa, parece que, pelo menos por enquanto, não é pela resolução do quebra-cabeça pelos sacerdotes que devemos nos preocupar 😌.
18.8. Exercícios¶
Mofifique a função
finonacciR()
para que retorne, além do número de Fibonacci, o número de adições feitas durante o processo recursivo.Determine o número exato de adições feitas pela execução de
fibonacciI(n)
.Escreva uma versão da função
fibonacciR()
que retorna dois números de Fibonacci consecutivos e com isso faz apenas uma chamada recursiva em cada nível da recursão. Faça experimentos para determinar qual das versões é mais rápida.Estime o número de adições feitas pela chamada
fibonacciRM(n)
.Escreva uma versão da função
fibonacciRM()
que utiliza como cache um array do NumPy (numpy.ndarray
) em vez de uma lista do Python (list
). Faça experimentos para determinar qual das versões é mais rápida, a que usa uma lista ou a que usa um array. Qual possível vantagem listas tem em comparação a arrays?Em termos de tempo, qual função você acha que calculará mais rapidamente a seguinte sequência de números de Fibonacci, a função
FibonacciI()
ouFibonacciRM()
? A sequência é: \(\mathtt{F_{1000}, F_{762}, F_{80}, F_{812}, F_{564}, F_{700}, F_{2}, F_{212}, F_{1}}\).Em termos de espaço, qual função você acha que gastará menos espaço para calcular a seguinte sequência de números de Fibonacci, a função
FibonacciI()
ouFibonacciRM()
? A sequência é \(\mathtt{F_{1000}, F_{762}, F_{80}, F_{812}, F_{564}, F_{700}, F_{2}, F_{212}, F_{1}}\).Escreva uma função que recebe um número não negativo
n
e retorna o número de nós internos da árvore da recursão para o cálculo de \(\mathtt{F_n}\) associada a chamadaFibonacciR(n)
.Escreva uma função que recebe um número não negativo
n
e retorna o número de folhas da árvore da recursão para o cálculo de \(\mathtt{F_n}\) associada a chamadaFibonacciR(n)
.Dê argumentos que comprovem que \(\mathtt{2^n-1}\) é o menor números de movimentos possível para resolver o quebra-cabeça das Torres de Hanoi com
n
discos.Escreva uma função recursiva
binomialR()
que recebe dois números inteiros não-negativosn
ek
e retorna \(\mathtt{\binom{n}{k}}\).binomialR()
deve ser uma tradução para Python da seguinte regra de Pascal :\[\begin{split}\binom{\mathtt{n}}{\mathtt{k}} = \begin{cases} \mathtt{0} & \mbox{quando $\mathtt{n = 0}$ e $\mathtt{k > 0}$}, \\ \mathtt{1} & \mbox{quando $\mathtt{n} \geq 0$ e $\mathtt{k = 0}$}, \\ \binom{\mathtt{ n}-1}{\mathtt{ k}} + \binom{\mathtt{n-1}}{\mathtt{k-1}}& \mbox{quando $\mathtt{ n}, \mathtt{{ k}>0}$}.\\ \end{cases}\end{split}\]Escreva uma função iterativa
binomialI()
que recebe dois dois números inteiros não-negativosn
ek
e retorna uma matriz de dimensão \(\mathtt{(n+1) \times (k+1)}\) em que cada posição[i][j]
tem o velor de \(\mathtt{\binom{i}{j}}\) para \(\mathtt{i=0,1,\ldots,n}\) e \(\mathtt{j=0,1,\ldots,k}\).Dica: Para fazer o seu trabalho
binomialI()
pode inicialmente preencher a linha 0 e coluna 0 da matriz com valores sabidos e em seguida percorrer linha a linha a matriz aplicando a regra de Pascal:\[\binom{n}{k} = \binom{n-1}{k} + \binom{n-1}{k-1}.\]Escreva o par de funções
binomialRM()
ebinomialRCache()
que formam a versão com memória da funçãobinomialR()
. A funçãobinomialRM()
recebe dois números inteiros não-negativosn
ek
e retorna uma matrizcache
de dimensão \(\mathtt{(n+1) \times (k+1)}\) em que cada posição[i][j]
tem o valor de \(\mathtt{\binom{i}{j}}\) para \(\mathtt{i=0,1,\ldots,n}\) e \(\mathtt{j=0,1,\ldots,k}\). A funçãobinomialRCache()
deve ser recursiva e, além dos inteirosn
ek
, recebe uma matrizcache
de de dimensão \(\mathtt{(n+1) \times (k+1)}\) e retorna \(\mathtt{\binom{n}{k}}\), preenchendo com \(\mathtt{\binom{i}{j}}\) toda posição[i][j]
decache
necessária durante os cálculos de \(\mathtt{\binom{n}{k}}\).Reescreva as funções
binomialI()
,binomialRM()
ebinomialRCache()
para que utilizemndarray
s do NumPy em vez de matrizes representadas como lista de listas,list[list]
. Quais são as possíveis vantagens dessas novas versões? Quais são as possíveis desvantagens?Escreva uma função recursiva
binomialRC()
que recebe dois números inteiros não-negativosn
ek
e retorna \(\mathtt{\binom{n}{k}}\). A funçãobinomialRC()
deve ser baseada na seguinte igualdade:\[\binom{n}{k} = \frac{n!}{(n-k)! k!} = \frac{(n-1)!}{(n-k)!(k-1)!} \times \frac{n}{k} = \binom{n-1}{k-1} \times \frac{n}{k}\]A estrutura da função recursiva
binomialRC()
, como a da funçãofatorialR()
e de outras que vimos, é chamada de recursão de calda (tail recursion).Faça uma análise experimental das funções
binomiaR()
,binomiaRC()
,binomialI()
ebinomialRM()
para estimar as suas limitações em termos de tempo gasto, espaço de memória utilizado e recursos com recursão que são necessários. Determine as possíveis vantagens e desvantagens da utilização de cada uma dessas funções.
18.9. Para saber mais¶
- Recursão e algoritmos recursivos de Projeto de Algoritmos de Paulo Feofiloff.
- Recursão de Resolução de Problemas com Algoritmos e Estruturas de Dados usando Python de Brad Miller e David Ranum.
- Recursão de Como Pensar Como um Cientista da Computação de Brad Miller e David Ranum.
- Recursion de Introduction to Programming in Python. de Robert Sedgewick, Kevin Wayne e Robert Dondero.