Recursão e Indução na Torre de Hanói
Esta é a descrição adaptada de um desafio proposto aos alunos no semestre 2011.1, com base no puzzle chamado de Torre de Hanói. Adaptamos o “desafio” aqui para servir como material de estudo, mas, se você quiser, pode tentar resolver as questões, antes de ler as respostas.
1. O Jogo
Neste jogo (ou neste problema), existem 3 pinos
e N discos furados ao meio (ou argolas) de diferentes tamanhos.
Inicialmente, todos os n discos estão empilhados no primeiro pino, ordenados do menor para o maior disco, com o menor no topo. O objetivo é mover todos os discos até o terceiro pino (o segundo é auxiliar), seguindo estas regras:
1. você só pode mover um disco por vez, e
2. você não pode colocar um disco maior sobre um menor.
Vocês podem tentar resolver instâncias deste problema pelo link: http://www.novelgames.com/flashgames/game.php?id=31
2. O Desafio
São quatro questões a serem respondidas/resolvidas:
1) Proponha um algoritmo recursivo informal para resolver o problema
O algoritmo deve ser capaz de realizar corretamente a seqüência de movimentos que leva todos os discos do pino inicial ao pino final.
A sugestão é que você siga uma idéia recursiva. Proponha um algoritmo informal recursivo que faça "todos os vários movimentos necessários para levar os N discos do topo, de um pino X para um pino Y, usando o pino Z como auxiliar".
A recursão pode ser feita com base no número N de discos a serem movidos. Para descrever um algoritmo recursivo, lembre-se de descrever:
• Como resolver o caso base: Que é trivial, para este problema. Deve ser o movimento para N=1 disco.
• Como resolver o caso recursivo: Que consiste em (recursivamente) efetuar vários movimentos que usem menos discos. Não necessariamente pense em mover um disco por vez, mas pense em mover (mais de uma vez) “blocos” de discos, cada um como menos do que N discos.
2) Implemente em Python a sua idéia de algoritmo
A dica que dou é que você pode representar os n discos como números de 1 a n e os pinos como listas. O disco do topo do pino pode ser o último valor da lista.
A configuração inicial para N=5, por exemplo, seria dada pelas três listas: [5, 4, 3, 2, 1] [ ] [ ]
[ ] [ ] [5, 4, 3, 2, 1]
3) Conjecture quantos movimentos o programa faz para mover N discos
Por exemplo: será que o programa faz (N-3)! / 2 movimentos?
Para essa questão, você não precisa ter o programa pronto em Python. Você pode resolver com base no algoritmo informal do passo 1.
4) Prove que a conjectura está correta
Você vai provar a sua conjectura (da questão 3) por indução.
3. Dicas e Respostas
Comentário 1
Lembre-se que eu quero um algoritmo recursivo! Vou aqui dar uma idéia, chamando o algoritmo de MOVE(N, Ini, Fim). O objetivo dele é "mover N discos do pino Ini para o pino Fim (usando o outro pino como auxiliar)".
Quando N=1, basta mover o disco do topo diretamente.
Nos outros casos, vou mover 1 disco (o topo) para o pino auxiliar, depois movo os N-1 restantes para Fim (como desejado) e depois movo de volta o disco do pino auxiliar para o pino Fim (como desejado). Com isso, todos os discos estarão no pino Fim na ordem correta.
Segue uma descrição em pseudocódigo desta idéia:
MOVE(N, Ini, Fim)
Aux = pino diferente de Ini e Fim
if N==1
mover 1 disco (o topo) de Ini para Fim
else
mover 1 disco (topo) de Ini para Aux
MOVE (N-1, Ini, Fim, Aux) ← recursão! mover 1 disco (topo) de Aux para Fim
Comentário 2
O problema com o algoritmo anterior é que ele coloca um disco grande em cima de um pequeno, no caminho até a configuração final.
Vamos tentar chegar a um algoritmo correto observando as soluções de alguns casos. Vamos começar com a solução da Torre de Hanoi para apenas 3 discos:
Numeramos as configurações (situações) do problema de 0 a 7, sendo 0 a configuração inicial. Como cada jogada causa uma mudança de configuração, vamos referenciar cada jogada dizendo a configuração inicial e a configuração final da jogada por isso. Por exemplo, a jogada 0-1 consistiu em levar o disco menor do pino 1 para o pino 3. Veja, ainda, que 7 é a quantidade total de jogadas efetuadas para resolver esta instância do problema.
Agora vamos analisar os movimentos feitos, com especial atenção ao disco maior. Veja que o disco maior só foi movido uma vez: na jogada 3-4, que o moveu do pino 1 para o pino 3. Isso divide a nossa seqüência de jogadas em duas partes:
• Depois, todas as jogadas restantes (jogadas 4-5, 5-6 e 6-7) foram análogas, porém o objetivo final foi mover os dois discos menores do pino 2 para o pino 3. (O pino 1 serviu de auxiliar em algum momento). Isso levou todos os discos,
na ordem correta, ao pino 3, resolvendo o problema.
Você agora já consegue usar a descrição acima para propor um algoritmo geral?
Se não conseguir, tente resolver o e analisar a solução do problema com 4 discos para chegar a uma idéia.
Comentário 3
Agora, vamos analisar a solução da Torre de Hanoi com 4 discos. Vamos mostrar que a idéia é parecida. Abaixo mostramos as 16 configurações, que representam as 15 jogadas necessárias para resolver esta instância do problema:
• Antes da jogada 7-8, o objetivo foi apenas mover os 3 discos menores do pino 1 para o pino 2. Isso liberou o disco maior para ser movido para o pino
desejado (o pino 3), logo em seguida.
• Depois da jogada 7-8, todas as jogadas feitas visaram apenas mover os 3 discos menores do pino 2 para o pino 3. Isso levou à configuração desejada.
Veja que, para resolver cada uma das duas partes acima, podemos adaptar os passos da solução do problema da Torre de Hanoi para 3 discos, que vimos antes. A diferença é que, em cada parte acima, mudamos o pino de origem e o pino de destino dos discos, mas as jogadas são análogas às da solução apresentada antes.
Veja que foram necessários 15 movimentos porque fazemos 7 para resolver cada uma das duas partes citadas (como vimos na solução do problema com 3 discos), o que já dá 14 movimentos. Além disso, entre as duas partes, fazemos 1 movimento do disco maior. Isso completa 15 movimentos.
De modo geral, veja que, para resolver o problema com N discos, precisamos “reusar” a idéia de solução para N-1 discos (no caso, reusamos duas vezes). Esse “reuso” da mesma idéia é típico de problemas que podem ser resolvidos recursivamente. E então, já sabe descrever o algoritmo completo?
Em pseudo-código, ele fica assim:
MOVE(N, Ini, Dest)
Aux = pino diferente de Ini e Dest
if N==1
mover (o disco do topo) de Ini para Dest
else
MOVE (N-1, Ini, Aux) (move N-1 discos de Ini para Aux,
usando Dest como auxiliar)
mover de Ini para Dest (move o disco maior, que é o
último que restou em Ini)
Comentário 4
Agora, vamos analisar a quantidade de jogadas (ou movimentos) que o algoritmo anterior faz quanto há n discos, no total.
Seja C(n) o número de movimentos necessários para mover uma pilha de n discos de um pino para outro. Observando o algoritmo anterior, vemos que:
• Quando n=1, teremos apenas que mover um único disco uma única vez. Ou seja, a quantidade de movimentos é unitária:
C(1) = 1 (caso base)
• Quando n>1, nós olhamos para o caso recursivo do algoritmo (descrito no corpo do “else”). Veja que, neste caso, para mover n discos, o algoritmo move n-1 discos, depois move 1 disco, depois move n-1 discos novamente. Logo:
C(n) = C(n-1) + C(1) + C(n-1) = C(n-1) + 1 + C(n-1) Ou, simplesmente:
C(n) = 2.C(n-1) + 1 (caso recursivo ou relação de recorrência)
Logo, como conseqüência direta da definição do algoritmo, a quantidade de movimentos C(n) que o algoritmo realiza é dada por:
C(1) = 1
C(n) = 2.C(n-1) + 1
Já a forma fechada desta função (que você deve ter conjecturado por si só, examinando as soluções com diferentes discos) é esta:
D(n) = 2n – 1.
Vamos provar por indução que C(n)=D(n) para todo n inteiro positivo:
Caso base n = 1
Neste caso temos apenas que mover o único disco de um pino para o outro,
C(1) = 1
D(1) = 21 - 1 = 2 – 1 = 1 Logo:
C(1) = D(1) (Caso base provado)
Passo indutivo
Hipótese: C(k) = D(k), para k ≥ 1 Objetivo: C(k+1) = D(k+1)
Vamos calcular o valor de C(k+1) usando a relação de recorrência (porque
k+1≥2):
C(k+1) = 2.C(k) + 1
Usando a hipótese e a definição de D(k), podemos reescrever assim: C(k+1) = 2.D(k) + 1
= 2.(2k – 1) + 1 = 2.2k – 2 + 1 = 2k+1 – 1
Como, por definição, D(k+1) = 2k+1 – 1, concluímos que: C(k+1) = D(k+1)
(Provado).
Assim, provamos que C(n) = 2n – 1. Portanto, podemos prever a quantidade de movimentos que o algoritmo faz para qualquer quantidade n de discos:
• Para 3 discos, ele faz 7 movimentos (como vimos no comentário 2)
• Para 4 discos, ele faz 15 movimentos (como vimos no comentário 3)
• Para 5 discos, ele faz 31 movimentos
• ...
• Para 9 discos, ele faz 511 movimentos
Não provamos, mas o algoritmo dado faz a quantidade mínima de movimentos legais possível. Portanto, não é boa idéia tentar resolver este problema por conta própria (sem usar o computador) para mais de 5 discos!
“E haverá grandes terremotos, epidemias e fome em vários lugares,
coisas espantosas e também grandes sinais do céu.
E por se multiplicar a iniqüidade, o amor se esfriará de quase todos.”