Dans les exercices, on pose également des questions relatives au temps de calcul. Ne négliger pas le travail sur ces questions,
il s'agit d'un thème essentiel.
Programmation
Il est clair que l'on sait résoudre le problème s'il n'y a qu'un seul anneau.
Si l'on sait déplacer n-1 anneaux d'un piquet à un autre, alors on sait déplacer n anneaux :
- on déplace les n-1 anneaux du dessus du piquet de départ vers l'intermédiaire,
- on déplace l'anneau restant sur le piquet de départ (c'est le plus grand anneau) vers le piquet objectif.
- on déplace les n-1 anneaux du piquet intermédiaire vers l'objectif.
Traduction en langage python :
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)
n=3
hanoi(n)
On obtient :
Déplacer l'anneau 1 du piquet départ vers le piquet objectif.
Déplacer l'anneau 2 du piquet départ vers le piquet intermédiaire.
Déplacer l'anneau 1 du piquet objectif vers le piquet intermédiaire.
Déplacer l'anneau 3 du piquet départ vers le piquet objectif.
Déplacer l'anneau 1 du piquet intermédiaire vers le piquet départ.
Déplacer l'anneau 2 du piquet intermédiaire vers le piquet objectif.
Déplacer l'anneau 1 du piquet départ vers le piquet objectif.
Nombre de déplacements
Notons \( T_n \) le nombre de déplacements de disques lors d'un appel à la fonction hanoi(n)
(en toute rigueur, il faut se convaincre que les valeurs des autres paramètres n'influencent pas le nombre
de déplacements de disques).
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 : \[ 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.
Vous pourrez lire également cet article.
Estimation du temps
Avec le programme suivant, on "mesure" le temps utilisé par la machine sur laquelle on exécute 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)
a=timeit(stmt='hanoi(15)',setup="from __main__ import hanoi",number=100)
print("Avec 15 : ", a)
On conjecture que le temps d'exécution est à peu près proportionnel au nombre de déplacements de disques.
Pour 20 disques par exemple, 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)
Une expérimentation sur ma machine m'a donné ceci :
Avec 15 : 1.431151767999836
a*2^5 : 45.79685657599475
Avec 20 : 45.39163709599961
La plausibilité de la conjecture est renforcée...
Estimons alors le temps pour 64 disques.
Le temps nécessaire pour 20 disques doit être multiplié par environ \( 2^{44} \).
Pour 20 disques, le temps d'exécution est d'environ 0,45 secondes.
\[ \frac{0.45 \times 2^{44}}{60\times 60\times 24 \times 365} \approx 251030 \]
Plus de 250 000 ans pour exécuter les déplacements avec 64 disques !
Raisonnons sur un échiquier 8*8.
Plaçons déjà la reine de la colonne 1. Elle peut être placée sur n'importe quelle ligne.
Notons ['1', '2', '3', '4', '5', '6', '7', '8'] la liste des lignes sur lesquelles elle peut être placée.
Cherchons maintenant à placer une reine en colonne 2. Pour chaque emplacement possible de la reine de la colonne 1, on dresse
la liste des emplacements possibles pour la reine de la colonne 2.
Par exemple pour l'emplacement reine colonne 1 en ligne 1, la reine de la colonne 2 peut occuper
toute ligne sauf les lignes 1 et 2.
R | non |
| non |
| ok |
| ok |
| ok |
| ok |
| ok |
| ok |
Dans la liste des 'solutions' pour deux reines, nous aurons donc déjà '13', '14', '15', '16', '17', '18'.
Lorsque la reine de la colonne 1 est en ligne 2, la reine de la colonne 2 peut se placer partout sauf sur les lignes 1, 2 ou
3.
| non |
R | non |
| non |
| ok |
| ok |
| ok |
| ok |
| ok |
A la liste des solutions pour deux reines, on peut donc ajouter '24', '25', '26', '27', '28'.
On poursuit ainsi pour chacune des positions possibles de la reine 1. On obtient la liste complète suivante :
['13', '14', '15', '16', '17', '18', '24', '25', '26', '27', '28', '31',
'35', '36', '37', '38', '41', '42', '46', '47', '48', '51', '52', '53',
'57', '58', '61', '62', '63', '64', '68', '71', '72', '73', '74',
'75', '81', '82', '83', '84', '85', '86'].
Ayant obtenu la liste complète des solutions possibles pour les deux premières reines, comment construire la liste
pour les trois premières reines ?
Pour chacune des solutions pour les deux premières, on teste chacune des lignes pour la reine de la colonne 3 et on ne retient
que les positions telles que cette reine colonne 3 ne soit pas en prise avec les deux premières reines.
On obtient la liste suivante :
['135', '136', '137', '138', '142', '146', '147', '148', '152', '157', '158',
'162', '164', '168', '172', '174', '175', '182', '184', '185', '186', '241',
'246', '247', '248', '251', '253', '257', '258', '261', '263', '268', '271',
'273', '275', '281', '283', '285', '286', '314', '316', '317', '318', '352',
'357', '358', '362', '364', '368', '372', '374', '382', '384', '386', '413',
'415', '417', '418', '425', '427', '428', '461', '463', '468', '471', '473',
'475', '481', '483', '485', '514', '516', '518', '524', '526', '528', '531', '536',
'538', '571', '572', '574', '581', '582', '584', '586', '613', '615', '617', '625',
'627', '631', '635', '637', '641', '642', '647', '681', '682', '683', '685', '713',
'714', '716', '718', '724', '726', '728', '731', '736', '738', '741', '742', '746',
'748', '751', '752', '753', '758', '813', '814', '815', '817', '824', '825', '827',
'831', '835', '837', '841', '842', '847', '851', '852', '853', '857', '861', '862',
'863', '864'].
On a ainsi défini notre processus récursif.
Traduction en langage python :
def ok(c) :
"""
c='*254' signifie
reine colonne 1, ligne 2;
reine colonne 2, ligne 5;
reine colonne 3, ligne 4.
Retourne True si reines non en prise.
On teste seulement la dernière reine
par rapport aux précédentes.
L'étoile placée en début de c ne sert qu'à avoir
des indices de 1 à n correspondant à une
numérotation 'naturelle' des colonnes de 1 à n.
"""
n=len(c)-1 # nb de reines
rr=c[-1] # ligne de la reine à tester
for i, r in enumerate(c) :
if 0<i<n :
if r==rr : return False
elif i+int(r)==n+int(rr) : return False
elif i-int(r)==n-int(rr) : return False
return True
def LesSolutions(n) :
""" Retourne la liste des solutions
pour un échiquier n*n."""
def solut(n,r) :
"""
n : echiquier n*n.
r : numéro de la colonne où l'on cherche à placer une reine.
retourne la liste des emplacements possibles pour la
colonne r.
"""
if r==1 : return [str(i) for i in range(1,n+1)]
else :
S=[]
for j in solut(n, r-1) :
for k in range(1,n+1) :
if ok('*'+j+str(k)) :
S.append(j+str(k))
return S
return solut(n,n)
def afficher(solu, n) :
"""
Affichage d'une solution
pour un échiquier n*n.
La solution solu donnée en argument est sous la forme '*235...'
"""
print(' ',end=' ')
for k in range(1,n+1) : print(k,end='|')
print()
for l in range(1,n+1) :
print(l,end='|')
for c in range(1,n+1) :
if solu[c]==str(l) :
print('R',end='|')
else :
print(' ',end='|')
print()
n=8 # taille de l échiquier
S=LesSolutions(n) # liste des solutions
print("Le nombre de solutions : ", len(S), "\n") # nombre de solutions
print(S,"\n") # affichage des solutions sous la forme chaînes
afficher('*'+S[0],n) # affichage de la première solution
# de la liste sous la forme dessin de grille
On obtient pour S[0] :
1|2|3|4|5|6|7|8|
1|R| | | | | | | |
2| | | | | | |R| |
3| | | | |R| | | |
4| | | | | | | |R|
5| |R| | | | | | |
6| | | |R| | | | |
7| | | | | |R| | |
8| | |R| | | | | |
et pour la liste S (92 solutions) :
['15863724', '16837425', '17468253', '17582463',
'24683175', '25713864', '25741863', '26174835', '26831475', '27368514',
'27581463', '28613574', '31758246', '35281746', '35286471', '35714286',
'35841726', '36258174', '36271485', '36275184', '36418572', '36428571',
'36814752', '36815724', '36824175', '37285146', '37286415', '38471625',
'41582736', '41586372', '42586137', '42736815', '42736851', '42751863',
'42857136', '42861357', '46152837', '46827135', '46831752', '47185263',
'47382516', '47526138', '47531682', '48136275', '48157263', '48531726',
'51468273', '51842736', '51863724', '52468317', '52473861', '52617483',
'52814736', '53168247', '53172864', '53847162', '57138642', '57142863',
'57248136', '57263148', '57263184', '57413862', '58413627', '58417263',
'61528374', '62713584', '62714853', '63175824', '63184275', '63185247',
'63571428', '63581427', '63724815', '63728514', '63741825', '64158273',
'64285713', '64713528', '64718253', '68241753', '71386425', '72418536',
'72631485', '73168524',
'73825164', '74258136', '74286135', '75316824', '82417536', '82531746',
'83162574', '84136275']
Remarque
On peut facilement écrire ici une version non récursive de l'algorithme décrit précédemment.
def ok(c) :
""" c='*254' signifie
reine colonne 1, ligne 2;
reine colonne 2, ligne 5;
reine colonne 3, ligne 4.
Retourne True si reines non en prise.
On teste seulement la dernière reine par rapport aux précédentes.
L'étoile placée en début de c ne sert qu'à avoir
des indices de 1 à n correspondant à une numérotation 'naturelle' des colonnes de 1 à n.
"""
n=len(c)-1 # nb de reines
for i in range(1,n) :
if c[i]==c[n] : return False
elif i+int(c[i])==n+int(c[n]) : return False
elif i-int(c[i])==n-int(c[n]) : return False
return True
def resolution(n) :
""" Retourne la liste des solutions pour un échiquier n*n."""
solu=['*'+str(i) for i in range(1,n+1)]
for i in range(2,n+1) :
solut=solu[:]
solu=[]
for j in solut :
for k in range(1,n+1) :
if ok(j+str(k)) : solu.append(j+str(k))
return solu
def afficher(solu, n) :
""" Affichage d'une solution pour un échiquier n*n.
La solution solu donnée en argument est sous la forme '*235...' """
print(' ',end=' ')
for k in range(1,n+1) : print(k,end='|')
print()
for l in range(1,n+1) :
print(l,end='|')
for c in range(1,n+1) :
if solu[c]==str(l) :
print('R',end='|')
else :
print(' ',end='|')
print()
n=8 # taille de l échiquier
S=resolution(n) # liste des solutions
print("Le nombre de solutions : ", len(S), "\n") # nombre de solutions
print(S,"\n") # affichage des solutions sous la forme chaînes
afficher('*'+S[0],n) # affichage de la première solution
# de la liste sous la forme dessin de grille