Formation I.S.N.

Une récréation mathématique d'Édouard Lucas.

résoudre le problème des tours d'Hanoï

Dans le jeu des tours de Hanoï, on dispose de trois piquets : le piquet de départ, le piquet objectif et un piquet intermédiaire.

Au départ, n disques de rayons différents sont empilés sur le piquet de départ. Le disque de plus grand rayon est le disque le plus bas, le disque de plus petit rayon est le disque le plus haut.

Tout disque est posé sur un disque de plus grand rayon que lui. Et ceci devra être conservé lors des déplacements de disque : interdiction absolue de poser à un moment donné un disque sur un disque de rayon plus petit.

L'objectif est de déplacer les disques du piquet de départ vers le piquet objectif. On ne peut déplacer qu'un seul disque à la fois, la règle énoncée ci-dessus sur les rayons est à respecter et on peut bien entendu utiliser le piquet intermédiaire.

Donner une programmation récursive.

Entrée Un entier naturel n (nombre de disques sur la tour initiale). Les disques seront numérotés de 1 à n, le numéro d'un disque pourra être considéré comme son rayon.
Sortie Messages indiquant les déplacements à faire.
  • principe
  • un code
  • petite amélioration

Il est facile de traiter le cas n=1 correspondant à la présence d'un seul disque.

Si l'on dispose de 2 disques, on déplace le disque 1 du piquet 'départ' vers le piquet 'intermédiaire', puis le disque 2 du piquet 'départ' vers le piquet 'final' et enfin le disque 1 de 'intermédiaire' vers 'final.

Avec 3 piquets, on déplace 1 et 2 vers le piquet intermédiaire (par une suite de mouvements analogues au cas précédent) puis on déplace le 3 vers le piquet final et enfin 1 et 2 vers le piquet final.

On voit que le plus grand disque joue le même rôle que le sol vis-à-vis des autres disques : on peut déplacer les autres disques sans se préoccuper du plus grand disque, ce plus grand disque restera toujours au sol sauf dans son unique déplacement du piquet initial vers le final.

Le cas récursif est donc (avec n>1 disques) :

  • déplacer les n-1 disques du dessus vers le piquet intermédiaire.
  • déplacer le grand disque du piquet 'départ' vers le piquet 'final'.
  • déplacer les n-1 disques du piquet intermédiaire vers le piquet 'final'.

Une erreur : - déplacer le disque du dessus et se dire qu'on n' a plus qu'à en déplacer n-1 (par récursivité). Ce raisonnement ne fonctionne pas du fait que le petit disque ne joue pas, contrairement au grand disque, un rôle 'neutre' comme le grand disque. Il empêche les mouvements vers son piquet et le problème à n-1 disques obtenu n'est donc pas de même nature que celui de départ.


def hanoi(n, a='départ', b='intermédiaire', c='objectif', z=1):
	""" a piquet de départ.
	b piquet intermédiaire.
	c piquet d'arrivée.
	n nombre d'anneaux à déplacer.
	z anneau déplacé."""
	if n == 1 :
		print("Déplacer l'anneau {} du piquet {} vers le piquet {}.".format(z,a,c))
	else:
		hanoi(n-1, a, c, b)
		hanoi( 1, a, b, c, n)
		hanoi(n-1, b, a, c)
        
        
hanoi(4)

on obtient :

Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
Déplacer l'anneau 2 du piquet départ vers le piquet objectif.
Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.
Déplacer l'anneau 3 du piquet départ vers le piquet intermédiaire.
Déplacer l'anneau 1 du piquet objectif vers le piquet départ.
Déplacer l'anneau 2 du piquet objectif vers le piquet intermédiaire.
Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
Déplacer l'anneau 4 du piquet départ vers le piquet objectif.
Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.
Déplacer l'anneau 2 du piquet intermédiaire vers le piquet départ.
Déplacer l'anneau 1 du piquet objectif vers le piquet départ.
Déplacer l'anneau 3 du piquet intermédiaire vers le piquet objectif.
Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
Déplacer l'anneau 2 du piquet départ vers le piquet objectif.
Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.

Petite amélioration pour l'affichage :


def tourHanoi(n):
    
    tours = dict()
    tours['départ'] = [ j for j in range(n,0,-1) ]
    tours['intermédiaire'] = []   
    tours['objectif'] = []
    
    
    def affichage(lesTours) :
        
        d = len(tours['départ'])
        i = len(tours['intermédiaire'])
        o = len(tours['objectif'])
        hauteur = max(d, i, o)
        
        for h in range(hauteur, 0, -1) :
            if d >= h : print( tours['départ'][h-1], end=' ')
            else : print(' ', end=' ')
            if i >= h : print( tours['intermédiaire'][h-1], end=' ')   
            else : print(' ', end=' ')
            if o >= h : print( tours['objectif'][h-1]) 
            else : print(' ')
        print('\u2AE0 \u2AE0 \u2AE0')
                
        print()
                
                
    def hanoi(n,a='départ',b='intermédiaire',c='objectif',z=1):
        """ a piquet de départ.
        b piquet intermédiaire.
        c piquet d'arrivée.
        n nombre d'anneaux à déplacer.
        z anneau déplacé."""
        if n==1:
            
            print("Déplacer l'anneau {} du piquet {} vers le piquet {}.".format(z,a,c))
            
            anneau_deplace = tours[a].pop()
            tours[c].append(anneau_deplace)
                
            affichage(tours)


        else:
            
            hanoi(n-1, a,c,b )
            hanoi(1,a,b,c,  n)
            hanoi(n-1,b,a,c)
            
    hanoi(n)
        
tourHanoi(4)       

on obtient :

Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
2    
3    
4 1  
⫠ ⫠ ⫠

Déplacer l'anneau 2 du piquet départ vers le piquet objectif.
3    
4 1 2
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.
3   1
4   2
⫠ ⫠ ⫠

Déplacer l'anneau 3 du piquet départ vers le piquet intermédiaire.
    1
4 3 2
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet objectif vers le piquet départ.
1    
4 3 2
⫠ ⫠ ⫠

Déplacer l'anneau 2 du piquet objectif vers le piquet intermédiaire.
1 2  
4 3  
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
  1  
  2  
4 3  
⫠ ⫠ ⫠

Déplacer l'anneau 4 du piquet départ vers le piquet objectif.
  1  
  2  
  3 4
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.
  2 1
  3 4
⫠ ⫠ ⫠

Déplacer l'anneau 2 du piquet intermédiaire vers le piquet départ.
    1
2 3 4
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet objectif vers le piquet départ.
1    
2 3 4
⫠ ⫠ ⫠

Déplacer l'anneau 3 du piquet intermédiaire vers le piquet objectif.
1   3
2   4
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet départ vers le piquet intermédiaire.
    3
2 1 4
⫠ ⫠ ⫠

Déplacer l'anneau 2 du piquet départ vers le piquet objectif.
    2
    3
  1 4
⫠ ⫠ ⫠

Déplacer l'anneau 1 du piquet intermédiaire vers le piquet objectif.
    1
    2
    3
    4
⫠ ⫠ ⫠

Nombre d'opérations

Combien de déplacements de disques sont utilisés dans la procédure ?

Est-il possible de faire mieux (c'est à dire de déplacer la tour de disques d'un anneau à l'autre en utilisant moins de déplacements de disques) ?

  • réponse

Notons \( T_n \) le nombre de déplacements de disques lors d'un appel à la fonction hanoi(n).

Pour exécuter hanoi(n), on fait appel à hanoi(1) une fois et à hanoi(n-1) à deux reprises. hanoi(1) ne demande qu'un seul déplacement de disque (c'est le cas de base). Nous en déduisons la relation : \( T_{n} = 2\times T_{n-1} + 1 \).

En posant \( v_n = T_n +1 \), nous obtenons une suite \( (v_n) \) géométrique de raison 2 : on vérifie en effet facilement que \( v_n = 2v_{n-1} \). Nous avons donc \( v_n = 2^n\times v_0 \), c'est à dire \( v_n = 2^n \). Finalement : \[ \boxed{T_n = 2^n -1} \]

Peut-on faire mieux ?

Notons \( s_n \) le nombre minimal de déplacements de disques qu'il est possible de réaliser.

Nous savons déjà que : \( s_n \leqslant 2^n-1 \).

Avec n disques, pour déplacer le plus grand disque de l'anneau de départ vers l'anneau but, il faut avoir déplacer tous les autres anneaux vers l'anneau intermédiaire, c'est à dire avoir exécuté au moins \( s_{n-1} \) mouvements de disques.
On déplace ensuite le grand disque de l'anneau de départ vers l'anneau but : on a donc fait, à ce moment, au moins \( s_{n-1} +1 \) mouvements de disques.
On doit ensuite déplacer les n-1 plus petits disques vers l'anneau but, c'est à dire exécuter à nouveau au moins \( s_{n-1} \) mouvements de disques.

Au total, pour déplacer n disques, il est nécessaire de faire au moins \( 2 s_{n-1} +1 \) mouvements de disques. Nous avons donc : \( s_n \geqslant 2s_{n-1} +1 \). Et il est facile, à partir de cette inégalité, de démontrer par récurrence que l'on a \( s_n \geqslant 2^n-1 \). \( 2^n -1 \) est donc le nombre minimal de déplacements de disques pour déplacer la tour.

complexité expérimentale.

Citons Édouard Lucas :

« N. Claus de Siam a vu, dans ses voyages pour la publication des écrits de l'illustre Fer-Fer-Tam-Tam, dans le grand temple de Bénarès, au-dessous du dôme qui marque le centre du monde, trois aiguilles de diamant, plantées dans une dalle d'airain, hautes d'une coudée et grosses comme le corps d'une abeille. Sur une de ces aiguilles, Dieu enfila au commencement des siècles, 64 disques d'or pur, le plus large reposant sur l'airain, et les autres, de plus en plus étroits, superposés jusqu'au sommet. C'est la tour sacrée du Brahmâ. Nuit et jour, les prêtres se succèdent sur les marches de l'autel, occupés à transporter la tour de la première aiguille sur la troisième, sans s'écarter des règles fixes que nous venons d'indiquer, et qui ont été imposées par Brahma. Quand tout sera fini, la tour et les brahmes tomberont, et ce sera la fin des mondes ! »

La question est maintenant de savoir si l'utilisation d'un ordinateur précipite cette fin des mondes.

Avec le programme suivant, on "mesure" le temps utilisé par la machine pour déplacer 15 disques, le déplacement complet étant répété une centaine de fois.

On a supprimé les affichages (qui augmenteraient ici considérablement les temps d'exécution) pour les remplacer par une "action vide" (pass).


from timeit import timeit

def hanoi(n,a='départ',b='intermédiaire',c='objectif',z=1):
	""" a piquet de départ.
	b piquet intermédiaire.
	c piquet d'arrivée.
	n nombre d'anneaux à déplacer.
	z anneau déplacé."""
	
	if n==1:
		pass
		#print("Déplacer l'anneau {} du piquet {} vers le piquet {}.".format(z,a,c))
	else:
		hanoi(n-1,a,c,b)
		hanoi( 1,a,b,c, n)
		hanoi(n-1,b,a,c)
		
		
# temps en secondes nécessaire à 100 exécutions de hanoi(15) :
a=timeit(stmt='hanoi(15)',setup="from __main__ import hanoi",number=100) 
print("Avec 15 : ", a)

Sur ma machine, j'ai obtenu (temps en secondes de 100 exécutions de hanoi(15)) :

Avec 15 :  1.1126629309999316

En utilisant ce qui précède, évaluer le temps nécessaire aux déplacements des disques sur votre machine dans le cas d'une quinzaine de disques. En déduire une estimation du temps de déplacement des disques dans le cas d'une tour de 64 disques.

  • principe
  • réponse

On conjecture que le temps d'exécution est à peu près proportionnel au nombre de déplacements de disques.

Pour 15 disques, nous avons vu que le déplacement est d'environ \(2^{15} \). Comme \(2^{20} = 2^{5} \times 2^{15} \), pour 20 disques, on multiplie par environ \( 2^5 = 32 \) le nombre de déplacements par rapport au cas de 15 disques. Le temps d'exécution devrait donc être à peu près multiplié par 32 si notre conjecture est correcte.

Cherchons à tester expérimentalement cette hypothèse.


from timeit import timeit

def hanoi(n,a='départ',b='intermédiaire',c='objectif',z=1):
	""" a piquet de départ.
	b piquet intermédiaire.
	c piquet d'arrivée.
	n nombre d'anneaux à déplacer.
	z anneau déplacé."""
	
	if n==1:
		pass
		#print("Déplacer l'anneau {} du piquet {} vers le piquet {}.".format(z,a,c))
	else:
		hanoi(n-1,a,c,b)
		hanoi( 1,a,b,c, n)
		hanoi(n-1,b,a,c)
		
		

a=timeit(stmt='hanoi(15)',setup="from __main__ import hanoi",number=100) 
print("Avec 15 : ", a)
b=timeit(stmt='hanoi(20)',setup="from __main__ import hanoi",number=100) 
print(" a*2^5 :", a*2**5)
print("Avec 20 : ", b)

Sur ma machine, j'ai obtenu :

Avec 15 :  1.0495726690000993
 a*2^5 : 33.58632540800318
Avec 20 :  33.43142246599973

Après quelques essais, et en tenant compte des autres temps occupant le processeur, la proportionnalité entre \( T_n \) et le temps de calcul est fortement plausible.

Estimons maintenant le temps pour 64 disques.

On a \( T_{64} \approx 2^{44} \times T_{20} \).


from timeit import timeit

def hanoi(n,a='départ',b='intermédiaire',c='objectif',z=1):
	""" a piquet de départ.
	b piquet intermédiaire.
	c piquet d'arrivée.
	n nombre d'anneaux à déplacer.
	z anneau déplacé."""
	
	if n==1:
		pass
		#print("Déplacer l'anneau {} du piquet {} vers le piquet {}.".format(z,a,c))
	else:
		hanoi(n-1,a,c,b)
		hanoi( 1,a,b,c, n)
		hanoi(n-1,b,a,c)
		
		

b=timeit(stmt='hanoi(20)',setup="from __main__ import hanoi",number=100)  
print("Avec 20 : ", b)
c = 2**44 * b/100
print(" b/100 * 2^44 = ", c )
print("En années : ", c/(60*60*24*365.25) )

Sur ma machine :

Avec 20 :  33.920566560000225
 b/100 * 2^44 =  5967369176555.2
En années :  189094.5184854108

Environ 190 000 années pour nos 64 disques !