Calcul du n-ème entier de Hamming

Michel Quercia
michel.quercia@prepas.org
Juin 1999

Note : la version HTML de ce document a été réalisée à partir du source LateX à l'aide du programme TtH (un petit bijou). La traduction n'est pas parfaite et la qualité typographique est sans rapport avec celle obtenue par LateX, ceci à cause des limitations du format HTML. Si les formules mathématiques sont franchement illisibles vérifiez que ce n'est pas un problème de réglage de fontes dans votre brouteur Internet (voir la documentation en ligne de TtH à ce sujet). Dans la mesure du possible consultez plutôt la version postscript : http://pauillac.inria.fr/~quercia/papers/hamming.ps.gz.

Table des matières

1  Introduction
2  Calcul des n premiers entiers de Hamming
    2.1  Calcul paresseux
    2.2  Files de priorité
        2.2.1  Ensembles ordonnés
        2.2.2  Listes mutables
    2.3  Classement sans comparaison
3  Méthodes géométriques
    3.1  Tétraèdres de Hamming
    3.2  Recherche par comptage
    3.3  Comptage et classement simultanés
    3.4  Arbres différentiels
    3.5  Méthode volumétrique
        3.5.1  Calcul de surface
        3.5.2  Calcul de volume
4  Conclusion
5  Code des programmes
    5.1  Listes paresseuses
    5.2  Files de priorité
    5.3  Listes mutables
    5.4  Classement sans comparaison
    5.5  Recherche par comptage
    5.6  Comptage et classement simultanés
    5.7  Arbres différentiels
    5.8  Méthode volumétrique

Chapitre 1
Introduction

Le problème de Hamming est le suivant : Un entier de Hamming est un entier naturel dont les seuls diviseurs premiers sont 2, 3 et 5. Écrire un programme imprimant dans l'ordre les n premiers entiers de Hamming.

Ce problème est présenté comme un exercice de programmation et comme une illustration du calcul paresseux. Il a été introduit par Dijkstra (A discipline of programming, Prentice Hall 1976, pp. 129-134). L'algorithme naïf qui consiste à passer en revue tous les nombres entiers dans l'ordre en ne retenant que ceux de la forme p = 2x3y5z (ce qui peut être testé en divisant p par 2, 3, 5 autant de fois que possible et en vérifiant que le dernier quotient vaut 1) n'est pas raisonnable : le 2000-ème entier de Hamming est égal à 8 100 000 000, le 2001-ème à 8 153 726 976 c'est à dire qu'il faudrait tester plus de 53 millions de nombres avant d'augmenter la liste des entiers de Hamming d'un élément. Les algorithmes que l'on trouve sur Internet à ce sujet reposent sur l'équation :

H = {1} È2H È3H È5H
H désigne l'ensemble des entiers de Hamming. Cette équation doit être interprétée comme suit : H est le plus petit ensemble contenant 1 et stable par multiplication par 2, 3 ou 5 (on considère ici que 1 est un entier de Hamming ayant le rang zéro). En particulier le n-ème entier de Hamming est 1 si n = 0, sinon c'est le plus petit entier double ou triple ou quintuple d'un entier de Hamming de rang inférieur ou égal à n-1. Pour calculer le n-ème entier de Hamming il suffit donc de calculer les n-1 précédents, de les multiplier par 2, 3 et par 5 et de retenir le plus petit entier non encore vu parmi ces produits. On verra ci-dessous que cet algorithme peut être implémenté avec une complexité O(n) en temps d'exécution et O(n2/3) en mémoire (l'unité de temps est une opération entre entiers de Hamming et l'unité de mémoire est un entier de Hamming).

Une variante du problème de Hamming consiste à calculer le n-ème entier de Hamming le plus rapidement possible sans calculer nécessairement tous les précédents. Une interprétation géométrique des entiers de Hamming comme points à coordonnées entières de l'espace tridimensionnel classés selon la forme linéaire (x,y,z)® xln2 + yln3 + zln5 permet alors de déterminer efficacement une plage plus ou moins restreinte dans laquelle se trouve le n-ème point de Hamming et d'identifier ce point par recherche dans la plage déterminée ce qui conduit à un algorithme de recherche ayant une complexité O(n1/3lnn) en temps et O(lnn) en mémoire.

Ces deux problèmes de Hamming ont donné lieu à une discussion passionnée sur la liste de diffusion ups-info en 1998 avec présentation d'algorithmes de plus en plus performants et l'objet de ce rapport est de rendre compte de ces algorithmes. Les mesures de temps d'exécution et d'occupation mémoire données ci-après ont été faites un PC-Linux (Pentium 233-MMX, 64Mo de Ram) à partir d'une implémentation Ocaml de chaque algorithme compilée avec ocamlopt version 2.02. Les programmes sont listés intégralement au chapitre 5, et sont disponibles sur le Web à l'URL http://pauillac.inria.fr/~quercia/papers/hamming.tgz.

Chapitre 2
Calcul des n premiers entiers de Hamming

2.1  Calcul paresseux

Le principe du calcul paresseux consiste à différer un calcul jusqu'au moment où son résultat devient nécessaire pour continuer. Une fois le calcul effectué le résultat est conservé pour utilisation ultérieure sans re-calcul. En Ocaml, la bibliothèque Lazy implémente les valeurs paresseuses. On crée une telle valeur par l'expression :

let x = lazy(expr)

expr est une expression Ocaml quelconque. Cette expression n'est pas évaluée immédiatement, mais les instructions permettant son évaluation sont enregistrées dans x. On obtient la valeur de expr par l'expression :

let y = force(x)

A ce moment expr est évaluée et y est lié au résultat. Ce résultat est aussi conservé dans x et les appels ultérieurs force(x) le retourneront sans provoquer de re-calcul.

Le calcul paresseux permet d'implémenter des structures de données potentiellement infinies dont on connaît le début et le moyen de calculer la suite. Par exemple la liste H des entiers de Hamming est définie paresseusement comme ayant une tête égale à 1 et une queue égale à la fusion sans répétition de 2H, 3H et 5H. Le programme listp ici, dû à Pierre Weis, est d'une étonnante simplicité.

exécution :

bash$ time -f "temps=%es" ./listp 1000000
519381797917090766274082018159448243742493816603938969600000000000000000000000000000
temps=81.88s

Validité et complexité : On note 2h, 3h et 5h les listes mult (Int 2) h mult (Int 3) h, et mult (Int 5) h. A un instant donné on suppose qu'on a déjà calculé les n premiers éléments de h et que l'on a extrait a éléments de 2h, b éléments de 3h et c éléments de 5h avec a < n, b < n et c < n. Cette hypothèse est vérifiée au moment où l'on construit h avec n = 1, a = b = c = 0.

Lors du calcul du n-ème élément de h (élément de rang n+1) on dispose déjà des éléments de rangs a+1 de 2h, b+1 de 3h et c+1 de 5h ou on les obtient par au plus trois multiplications car a < n, b < n et c < n donc le n-ème élément de h s'obtient en temps borné (deux comparaisons et au plus trois multiplications). Ceci prouve déjà que l'appel nème n h termine au bout d'un temps O(n).

Ensuite, on prouve aisément par récurrence que tous les éléments de h sont des entiers de Hamming et qu'ils forment une suite strictement croissante. Enfin, si x est un élément de cette suite alors 2x figure dans h sinon tous les éléments de 3h et 5h seraient inférieurs à x ce qui contredit la propriété de croissance stricte. De même 3x et 5x figurent aussi dans h donc h est stable par multiplication par 2, 3 et 5 et contient 1 comme premier élément, c'est bien la suite de Hamming.

Complexité spatiale : à priori la complexité spatiale est aussi O(n) puisque chaque étape consome une quantité bornée de mémoire. En fait, la liste h étant locale au calcul du n-ème entier de Hamming, la mémoire occupée par le début de cette liste est récupérable par le gestionnaire de mémoire intégré à Ocaml au fur et à mesure que l'on progresse dans les listes 2h, 3h et 5h. Donc si x est le n-ème entier de Hamming seuls les entiers de Hamming compris entre x/5 et x-1 doivent être conservés pour permettre le calcul de x et des nombres suivants. On verra dans la partie 3.1 que le nombre de tels entiers est de l'ordre de n2/3 c'est à dire que le programme de P. Weis peut tourner avec O(n2/3) unités de mémoire. Expérimentalement une augmentation de n dans un rapport 8 produit une augmentation de la mémoire allouée dans un rapport de l'ordre de 4 (cf. tableau ci-dessous). Par contre, si h est déclarée au niveau global alors tous les termes calculés sont conservés et la mémoire consommée augmente linéairement avec n.

40 80 160 320 640 1280 2560 n (milliers)
listp 2.24 4.92 10.66 23.35 49.49 103.79 218.35 temps (s)
1412 1676 1936 2964 4500 7296 11600 mémoire (Ko)
listp1 2.90 6.41 13.58 31.11 101.17 temps (s)
4828 9720 18640 39356 91512 mémoire (Ko)

(* fichier listp1.ml : identique à listp.ml sauf la liste h qui est ici globale µ*)

open Lazy open Num

(* liste paresseuse d'entiers de Hamming *) type listp = hd:num; tl:listp Lazy.t

(* n-ème élément d'une liste paresseuse *) let rec nème n liste = if n = 0 then liste.hd else nème (n-1) (force liste.tl)

(* multiplie une liste par p *) let rec mult p liste = hd = liste.hd */ p; tl = lazy(mult p (force liste.tl))

(* fusion sans répétition de listes paresseuses *) let rec (@) a b = let c = compare_num a.hd b.hd in if c < 0 then hd=a.hd; tl=lazy((force a.tl) @ b ) else if c > 0 then hd=b.hd; tl=lazy( a @ (force b.tl)) else hd=a.hd; tl=lazy((force a.tl) @ (force b.tl))

(* calcul µ*) let rec h = hd = Int 1; tl = lazy((mult (Int 2) h) @ (mult (Int 3) h) @ (mult (Int 5) h)) let _ = let n = int_of_string(Sys.argv.(1)) in print_string(string_of_num(nème n h)); print_newline()



2.2  Files de priorité

Le programme de Pierre Weis a été le point de départ d'une compétition entre différents abonnés de la liste ups-info pour calculer le plus rapidement possible les n premiers nombres de Hamming. Les deux programmes qui suivent reposent sur les mêmes principes :

Il est clair qu'on ne produit ainsi que des entiers de Hamming et qu'on les produit par ordre croissant. On prouve qu'on les produit tous jusqu'au rang n par récurrence sur n : pour n = 0 il n'y a rien à prouver. Si l'hypothèse est vraie au rang n alors après extraction du n-ème élément, x, h contient tous les quintuples des entiers de Hamming compris entre x+1 et 5x et certains doubles et triples eux-aussi compris entre x+1 et 5x. Soit y le premier entier de Hamming suivant x ; il s'agit de prouver que y est dans h.

Le raisonnement précédent prouve aussi qu'un entier de Hamming n'est inséré qu'une seule fois dans h.

Remarque : une critique potentielle contre les deux programmes qui suivent est que les logarithmes étant calculés de façon approchée il est concevable qu'une erreur d'arrondi provoque un mauvais classement. Expérimentalement ce n'est pas le cas pour le premier million d'entiers de Hamming mais une justification théorique semble délicate. On pourrait implémenter une comparaison approchée avec contrôle de précision et recourir à l'arithmétique exacte en cas de précision insuffisante au prix d'une dégradation sévère de la vitesse de calcul.

2.2.1  Ensembles ordonnés

Une première méthode consiste à implémenter h par un ensemble à l'aide de la bibliothèque Set de Ocaml. Cette bibliothèque représente les ensembles sur un type totalement ordonné par des arbres de recherche équilibrés, donc les temps d'insertion d'un élément et d'extraction du minimum sont logarithmiques en la taille de l'ensemble. Dans le cas présent, comme h ne contient que des nombres de Hamming compris entre x+1 et 5x où x est le dernier élément extrait, la taille de h est O(n2/3) où n est le rang du dernier nombre extrait donc la gestion de h a un coût temporel O(lnn) pour une opération (insertion ou extraction d'un élément) et le temps total de calcul des n premiers entiers de Hamming un coût temporel O(nlnn). La complexité spatiale est O(n2/3). Le programme correspondant est listé ici.

exécution :

bash$ time -f "temps=%es" ./prifile 1000000
2^38 . 3^109 . 5^29
temps=37.31s

mesures de temps et mémoire :

40 80 160 320 640 1280 2560 n (milliers)
1.20 2.49 5.30 11.21 22.78 47.25 98.12 temps (s)
1084 1308 1588 1840 2600 3860 5624 mémoire (Ko)

2.2.2  Listes mutables

La deuxième implémentation de h, proposée par Denis Cazor, utilise une liste mutable (c'est à dire une liste dont les pointeurs de chaînage sont mutables de façon à pouvoir insérer un élément en milieu de liste sans recopier le début de la liste), classée par ordre croissant. De plus Denis remarque que lorsqu'on insère 5t dans h alors c'est le dernier élément de h donc on peut le placer directement à la fin pourvu qu'on dispose d'un pointeur, z, vers le dernier élément de h. Pour insérer 3t on peut partir de la dernière position, y, où l'on a inséré un triple 3t¢ car nécessairement t¢ < t. On progresse à partir de y jusqu'à trouver deux entiers encadrant 3t et on l'insère à cet endroit en mettant à jour y. L'insertion d'un double se fait de même à l'aide d'un pointeur, x, mémorisant la dernière position où l'on a inséré un double. Voir le listing ici.

Remarques :

exécution :

bash$ time -f "temps=%es" ./cazor 1000000
2^38 . 3^109 . 5^29
temps=7.51s

mesures de temps et mémoire :

40 80 160 320 640 1280 2560 n (milliers)
0.22 0.44 0.92 2.01 4.35 9.10 19.03 temps (s)
1084 1352 1376 1588 2400 3164 4208 mémoire (Ko)

Complexité temporelle : Les mesures précédentes laissent penser que la complexité temporelle du programme de Denis est linéaire par rapport à n. Ceci n'est pas évident au vu du code car le temps de recherche de la position d'insertion d'un double ou un triple est difficile à borner de façon linéaire : il y a O(n2/3) entiers de Hamming inférieur au n-ème non divisibles par 5 et la liste h est de taille O(n2/3) donc le temps cumulé des insertions de doubles et triples est O(n4/3).

En fait si t est le n-ème nombre de Hamming alors les pointeurs x et y n'auront pas dépassé le nombre 5t et le nombre d'entiers de Hamming inférieurs à 5t est O(n) donc le temps cumulé des insertions de doubles et triples est effectivement O(n). Par ailleurs, on peut remarquer que lorsqu'on doit insérer un double, 2t, alors t est une puissance de 2 et x pointe vers le dernier double inséré c'est à dire vers t qui est aussi pointé par h. On pourrait donc supprimer la gestion de x dans le programme, expérimentalement cela n'apporte pas d'accélération sensible.

2.3  Classement sans comparaison

L'algorithme suivant, listé ici et proposé par Damien Doligez, reprend la méthode de Pierre Weis : la liste H des entiers de Hamming est prolongée élément par élément en prenant le plus petit des entiers non encore sélectionnés dans 2H, 3H et 5H, ces listes étant elles-mêmes déduites de H. La première différence avec la méthode de Pierre Weis est qu'il n'y a pas de code stocké en bout de liste mais seulement un pointeur prêt à être modifié pour pointer vers les éléments ajoutés et c'est le code du programme principal qui ajoute ces éléments l'un après l'autre par une simple boucle. On économise ainsi la création et la gestion des suspensions (code à exécuter plus tard et environnement associé) d'où une charge machine moindre et une exécution plus rapide. Par rapport aux méthodes avec file de priorité on économise le calcul inutile des doubles, triples ou quintuples dépassant le n-ème entier de Hamming.

listinf.gif
Le principe de fonctionnement est illustré par la figure ci-dessus : La liste H de Hamming est en cours de construction et h pointe vers le dernier élément calculé ; x pointe vers le premier élément, 2x, de la liste 2H non encore utilisé, cet élément pointant vers l'élément x de H dont il est issu ; de même pour y, 3y, z et 5z. On fait progresser la construction en plaçant derrière la cellule pointée par h le nombre m = min(2x,3y,5z), en faisant avancer h d'un cran, et en mettant à jour les cellules pointées par x, y et z selon que m = 2x, m = 3y ou m = 5z (ces cas ne sont pas exclusifs). Au bout de n étapes h pointe vers le n-ème nombre de Hamming. La démonstration de validité de cet algorithme est identique à celle pour le programme de Pierre Weis.

Une deuxième amélioration présentée par Damien est une technique de calcul de m qui ne fait quasiment jamais appel à la comparaison entre entiers de Hamming. Il remarque que si 2x est divisible par 3 alors 2x = 3t et le nombre 3t figure dans 3H après 3y sinon il aurait déjà été placé dans la liste résultat (3y est le premier triple non placé). Donc 2x = 3t ³ 3y et par conséquent m = min(3y,5z). De même si 3y est un multiple de 5 alors il est supérieur ou égal à 5z d'où m = 5z et si 5z est un multiple de 3 alors m = 3y. On arrive ainsi à déterminer m sans comparaison en examinant les exposants de 2, 3 et 5 des entiers 2x, 3y et 5z au moins dans tous les cas où ces trois nombres ne sont pas primaires (ayant un seul facteur premier). En pratique cette technique de calcul rapide de m échoue seulement 52 fois sur le premier million d'entiers de Hamming.

exécution :

bash$ time -f "temps=%es" ./nocomp 1000000
2^38 . 3^109 . 5^29
temps=4.96s
mesures de temps et mémoire :

40 80 160 320 640 1280 2560 n (milliers)
0.18 0.33 0.69 1.46 3.04 6.25 13.17 temps (s)
888 908 1140 1392 1644 1896 2625 mémoire (Ko)

Chapitre 3
Méthodes géométriques

3.1  Tétraèdres de Hamming

Un entier de Hamming, 2x3y5z, peut être vu comme un point de l'espace de coordonnées x,y,z. L'ensemble des entiers de Hamming inférieurs ou égaux à une borne K correspond à l'ensemble des points à coordonnées entières dans le tétraèdre limité par les plans d'équations :

x = 0,       y = 0,       z = 0,       xlg2+ylg3+zlg5 £ lgK
où lg désigne le logarithme en base 2. Cette interprétation permet déjà d'évaluer approximativement le nombre n(K) d'entiers de Hamming majorés par K à l'aide du volume de ce tétraèdre :
v(K) = (lgK)3
6lg2lg3lg5
.
Chaque point entier dans le tétraèdre est le sommet le plus proche de l'origine d'un cube de côté 1 et la réunion de ces cubes recouvre le tétraèdre donc v(K) £ n(K). Mais tout sommet de cette réunion de cubes est inclus dans le tétraèdre limité par le plan d'équation :
xlg2+ylg3+zlg5 £ lgK+lg2+lg3+lg5 = lg(30K)
donc n(K) £ v(30K). On en déduit :
n(K) = (lgK)3
6lg2lg3lg5
+ O((lgK)2)
et, pour a > 1 constant :
n(aK) - n(K) £ v(30aK) - v(K) = O((lgK)2).

Si t est le n-ème nombre de Hamming et K = lgt alors n(K) = n+1 donc lgK ~ 3Ö{6nlg2lg3lg5} ce qui prouve que le nombre d'entiers de Hamming compris entre t et at est O(n2/3) pour a > 1 fixé. Les estimations de complexité des algorithmes de calcul des n premiers entiers de Hamming résultent de cette majoration.

3.2  Recherche par comptage

tetraedre.gif
La figure ci-dessus illustre cette méthode. Pour déterminer le n-ème entier de Hamming on compte le nombre de points dans un tétraèdre de Hamming de paramètre K = 2k par récurrence sur k et l'on fait progresser k tant que n(2k) < n. Soit k tel que n(2k-1) < n £ n(2k), O,A,B,C les sommets du tétraèdre de paramètre 2k-1 et O,A¢,B¢,C¢ ceux du tétraèdre de paramètre 2k. On sait donc que le n-ème nombre de Hamming est représenté par un point entier dans la région gris-clair, il suffit de classer tous les points entiers dans cette région selon l'ordre induit par celui des entiers de Hamming et de chercher l'élément de rang n(2k)-n en partant de la fin.

Puisqu'on utilise le logarithme en base 2, A et A¢ ont pour abscisses k-1 et k, donc l'épaisseur en x de la tranche ABCA¢B¢C¢ vaut 1 et tous les points entiers de cette tranche se projettent parallèlement à Ox de façon bijective sur les points entiers du triangle OB¢C¢. On en déduit la relation de récurrence :

n(2k) = n(2k-1) + p(2k)
où p(2k) est le nombre de points (y,z) entiers vérifiant les inéquations :
y ³ 0,       z ³ 0,       ylg3+zlg5 £ k.
En comptant ces points selon z on a :
p(2k) = ëk/lg5û
å
z = 0 
æ
è
1+ ê
ë
k-zlg5
lg3
ú
û
ö
ø
.

En supposant les calculs en flottant suffisamment précis, p(2k) est calculable en temps O(k) donc k et n(2k) sont calculables en temps O(k2) = O(n2/3). La tranche ABCA¢B¢C¢ contient O(n2/3) points entiers déduits de ceux du triangle OB¢C¢, ces points sont classables en temps O(n2/3lnn) et la recherche du point de rang n(2k)-n se résume à un parcours dans la liste classée d'où un temps O(n2/3). On peut donc déterminer le n-ème entier de Hamming en temps O(n2/3lnn) et en mémoire O(n2/3), le programme correspondant est listé ici.

exécution :

bash$ time -f "temps=%es" ./comptage 1000000
2^38 . 3^109 . 5^29
temps=0.54s
mesures de temps et mémoire :

1 2 4 8 16 32 64 n (millions)
0.54 1.05 1.79 3.16 5.75 9.59 17.17 temps (s)
2372 3052 4300 6480 9956 15128 23740 mémoire (Ko)

3.3  Comptage et classement simultanés

tranche.gif
Damien Doligez améliore la méthode précédente en réalisant simultanément le comptage des points dans les tétraèdres successifs et le classement de la dernière tranche, le tout en arithmétique exacte ! Il remarque qu'une tranche de Hamming (points compris entre les tétraèdres de paramètres 2k-1 et 2k) s'obtient à partir de la tranche précédente par translation d'une unité le long de l'axe des x et adjonction des points de la nouvelle tranche situés dans le plan yOz. En termes d'entiers de Hamming, cela signifie que l'ensemble des entiers de Hamming dans l'intervalle ]2k-1,2k] est la réunion des doubles des entiers de Hamming de l'intervalle précédent ]2k-2,2k-1] et des entiers de la forme 3y5z compris entre 2k-1 et 2k. On passe ainsi d'une tranche triée à la suivante par une translation qui ne modifie pas le classement et par insertion au bon endroit des points associés aux entiers de Hamming impairs. Ceux-cis peuvent être générés dans l'ordre par une adaptation des méthodes du chapitre précédent, il reste à déterminer s'ils appartiennent bien à la tranche en cours et à quel endroit les insérer.

Soit h = 3b5c un entier à insérer. On suppose b > 0 c'est à dire h divisible par 3. Donc t = h/3 est un entier de Hamming qui a été engendré avant h (dans la tranche précédente ou dans celle d'avant) et on dispose déjà dans la tranche en cours du nombre 2a t avec a = 1 ou a = 2. Soit 2b u l'entier de Hamming précédant 2a t avec u impair. On a déjà inséré 3u dans une tranche antérieure et on dispose donc de v = 2g.3u dans la tranche courante. Enfin on a 2b u < 2a t = 2a h/3 donc 2b-a-g v < h. On distingue alors plusieurs cas :

  1. b-a-g > 0 : alors h > 2v et v figure dans la tranche en cours donc h n'appartient pas à cette tranche.
  2. b-a-g = 0 : alors on a trouvé un élément de la tranche en cours minorant h.
  3. b-a-g < 0 : le minorant trouvé pour h est soit non entier soit dans une tranche précédente, on n'a pas obtenu d'information utile.

Dans les bons cas on obtient soit l'information que h est dans une tranche supérieure et donc celle en cours est terminée, soit un minorant probalemement assez proche de h. On peut de même trouver dans les bons cas un majorant de h en considérant le successeur de t, et un autre couple minorant-majorant si h est aussi divisible par 5. Enfin on dispose d'un troisième minorant, le dernier élément inséré dans la tranche en cours avant h ou le début de cette tranche.

Si l'un des majorants est le successeur immédiat de l'un des minorants alors on a trouvé la position d'insertion de h sans avoir fait de comparaison entre entiers de Hamming. Sinon, on peut comparer h au dernier élément de la tranche (2k) pour savoir si h appartient à cette tranche puis, dans l'affirmative, rechercher la position d'insertion de h par balayage entre l'un des minorants et l'un des majorants.

Expérimentalement cette technique de classement ne donne lieu qu'à environ 400 comparaisons entre entiers de Hamming pour la recherche du millionnième nombre de Hamming, 500 pour la recherche du milliardième, c'est à dire suffisament peu pour que le coût de ces comparaisons soit négligeable dans l'exécution de l'algorithme. On pourrait d'ailleurs le diminuer encore en testant avec les notations précédentes que b ³ a et en reculant ou avançant de plusieurs positions autour de t pour réaliser cette condition. On tombe alors à 63 comparaisons pour le millionnième nombre de Hamming et à 104 pour le milliardième mais le temps d'exécution n'est pas notablement modifié.

Implémentation (voir le listing ici) : un entier de Hamming est représente par ses trois exposants et une batterie de pointeurs :

De plus, l'exposant de 2 n'est pas codé directement, on code la différence avec l'exposant du dernier élément de la tranche (2k) car cette différence est inchangée lorsqu'on multiplie tous les éléments d'une tranche par 2 donc on économise ainsi la mise à jour des exposants, il suffit de conserver dans une variable auxilliaire l'exposant du dernier élément.

exécution :

bash$ time -f "temps=%es" ./compte-classe 1000000
2^38 . 3^109 * 5^29
temps=0.22s
mesures de temps et mémoire :

1 5 10 50 100 500 1000 n (millions)
0.22 0.59 0.97 3.13 5.14 15.88 26.60 temps (s)
1140 2068 2936 7300 11028 31376 49588 mémoire (Ko)

Complexité : expérimentalement la méthode de Damien tourne en temps O(n2/3) ce qui pourrait être justifié en prouvant que l'insertion d'un nouvel entier de Hamming impair se fait en temps borné. Une telle preuve reste à écrire. La complexité spatiale est la taille maximale d'une tranche, soit O(n2/3).

3.4  Arbres différentiels

bande.gif
Le principe de translation des tranches de Damien Doligez s'applique aussi en dimension 2 : les entiers de Hamming impairs compris entre 2k-1 et 3.2k-1 forment une bande dans le plan yOz qui s'obtient à partir de la bande précédente par translation d'une unité parallèlement à l'axe des y et adjonction éventuelle d'une puissance de 5. Cette remarque est à l'origine d'un nouvel algorithme de comptage :

Supposons disposer des entiers de Hamming impairs compris dans l'intervalle ]2k-1,3.2k-1], classés dans une structure de données particulière permettant d'implémenter efficacement les opérations d'insertion, de découpage par rapport à un pivot, de réunion sans recouvrement, de comptage et de translation d'exposants. On note Bk la bande contenant ces entiers. Alors on passe de Bk à Bk+1 par découpage de Bk en deux parties : Bk = B¢k ÈB¢¢k avec B¢k = Bk Ç]2k-1,2k] et Bk¢¢ = BkÇ]2k,3.2k-1], translation de B¢k d'une unité en y ce qui donne formellement B¢k+ln3, insertion éventuelle d'une puissance de 5 et fusion sans recouvrement avec B¢¢k. Ceci permet de déterminer de manière incrémentale :

Notons que Tk est obtenue comme réunion avec recouvrement de translatées des B¢i ce qui permet d'obtenir facilement son cardinal mais plus difficilement son élément de rang n(2k)-n par ordre décroissant (la méthode de classement de Damien Doligez réalisait à la fois la réunion et le classement de Tk).

Structure de données pour les Bk : on utilise un arbre binaire de recherche ce qui permet, si l'arbre est équilibré, d'effectuer l'insertion le découpage et la réunion sans recouvrement en temps logarithmique (pour la réunion on extrait le plus grand élément de l'arbre de gauche et on le prend comme racine de l'arbre réunion). Le comptage peut être réalisé en temps constant si l'on stocke dans chaque noeud le cardinal du sous-arbre issu de ce noeud et si l'on met à jour ce cardinal lors des opérations d'insertion, découpage et réunion. Enfin, la translation d'exposant peut être réalisée en temps constant si l'étiquette d'un noeud code les exposants du point associé par différence avec les exposants du père. Il suffit alors de translater les exposants de la racine pour translater tout l'arbre, là aussi la gestion de ce codage différentiel est intégrable dans les opérations d'insertion, découpage et réunion. Si les arbres obtenus sont équilibrés, toutes les opérations requises peuvent être effectuées en temps constant ou logarithmique donc le passage d'une tranche de Hamming à la suivante a une complexité en temps O(lnn) et le calcul complet des tranches successives jusqu'à trouver celle contenant le n-ème nombre de Hamming s'effectue en temps O(n1/3lnn). L'équilibre des arbres n'est cependant pas garanti, on pourrait maintenir l'équilibre en temps logarithmique par rotations puisqu'on dispose des cardinaux des branches, ceci n'a pas été fait dans le programme listé ici par souci de simplification.

Recherche du n-ème nombre de Hamming dans la dernière tranche : cette dernière tranche se présente comme la réunion de O(n1/3) bandes triées, on peut obtenir son élément de rang n(2k)-n par segmentation (on choisit un pivot, on découpe toutes les bandes par rapport à ce pivot en comptant combien d'éléments sont de chaque côté ce qui permet de savoir si le n-ème entier de Hamming se trouve dans une demi-bande gauche, dans une demi-bande droite ou est égal au pivot ; dans les deux premiers cas on itère la recherche sur les demi-bandes correspondantes). Pour déterminer le pivot servant à découper toutes les bandes on commence par isoler une bande de cardinal maximal et on recherche dans cette bande l'élément de rang ërc/tû où r est le rang cherché, c le cardinal de la bande et t le cardinal de la tranche. Cette technique s'apparente à la méthode d'interpolation linéaire mais elle n'en a pas les mêmes performances car après quelques découpages les bandes deviennent très petites et le pivot sélectionné peu pertinent. Une méthode plus efficace serait de calculer le maximum et le minimum des éléments restant en lice et de calculer un pivot par interpolation linéaire entre ces extrémums, mais l'ensemble des points de Hamming n'est pas stable par barycentrage et l'utilisation d'un pivot non entier (en calculant le barycentre des logarithmes) complique la validation des comparaisons entre deux nombres de Hamming ou un nombre de Hamming et un pivot ce qui fait que le gain réalisé sur le nombre d'itérations est perdu par des comparaisons plus lentes.

Comparaison entre entiers de Hamming : à la différence de la méthode de comptage et classement simultanés, la méthode décrite ci-dessus donne lieu à un grand nombre de comparaisons lors des découpages donc il importe d'implémenter efficacement ces comparaisons. La comparaison approchée de deux entiers de Hamming par l'intermédiaire de leurs logarithmes est séduisante mais sujette à contestations, ici on l'améliore en effectuant un contrôle de précision lors d'une comparaison et en basculant vers une comparaison exacte en cas de doute. Expérimentalement, pour trouver le 10p-ème nombre de Hamming il suffit de disposer d'encadrements de ln(3)/ln(2) et ln(5)/ln(2) à p+3 décimales pour ne pas rencontrer de cas douteux.

exécution (les lignes k=.. t=.. donnent pour chaque étape de segmentation le rang cherché et le nombre d'élements restant en lice) :

bash$ time -f "temps=%es" ./difftree 1G
k=1046828 t=1069703
k=1188 t=24063
k=1173 t=24048
k=1091 t=23966
k=1067 t=23942
k=57 t=22932
k=57 t=82
k=57 t=71
k=57 t=60
k=21 t=24
k=5 t=8
k=0 t=3
2^761 . 3^572 . 5^489
temps=3.27s
mesures de temps et mémoire :

1 5 10 50 100 500 1000 n (millions)
0.24 0.49 0.65 1.06 1.54 3.01 3.27 temps (s)
940 1232 1424 1944 2468 4088 4848 mémoire (Ko)

Complexité probable : le temps de construction de la tranche de Hamming est O(n1/3lnn) en supposant que les arbres différentiels restent équilibrés et le temps d'une segmentation est O(n1/3lnn) également. En supposant que la méthode de segmentation retenue conduit à O(lnn) itérations, on obtient une complexité temporelle pour le calcul du n-ème nombre de Hamming en O(n1/3(lnn)2), l'unité de temps étant une opération entre entiers, mais cette majoration repose sur des hypothèses difficiles à établir. Par ailleurs, si les approximations de ln(3)/ln(2) et ln(5)/ln(2) à O(lnn) chiffres sont effectivement suffisantes pour éviter les calculs exacts alors le coût d'une opération entre entiers est O((lnn)2) d'où une complexité probable O(n1/3(lnn)4) . Remarquons que la complexité spatiale est sous les hypothèses précédentes O(n1/3(lnn)3) alors qu'on a au cours du calcul engendré une tranche de cardinal O(n2/3)...

3.5  Méthode volumétrique

Cette méthode est la plus performante de toutes celles présentées, elle fournit le n-ème entier de Hamming en temps O(n1/3(lnn)2) et en mémoire logarithmique (l'unité de temps est une opération entre entiers et l'unité mémoire un entier ; si l'on tient compte de ce que les entiers ont une taille machine de l'ordre de lnn alors la complexité temporelle est O(n1/3(lnn)4) et la complexité mémoire O((lnn)2)). Sur un micro-ordinateur bas de gamme on peut ainsi déterminer le 1018-ème entier de Hamming en moins de deux heures, ce qui est impossible avec les méthodes précédentes pour cause de manque de mémoire.

Le principe est le suivant : pour a,b,c réels strictement positifs et N réel quelconque on note

S(a,b,N)
=
card {(x,y) Î N2 / ax+by £ N}
V(a,b,c,N)
=
card {(x,y,z) Î N3 / ax+by+cz £ N}.
S(a,b,N) est la <<surface discrète>> du triangle limité par les deux axes de coordonnées et la droite d'équation ax+by = N, V(a,b,c,N) est le <<volume discret>> du tétraèdre limité par les trois plans de coordonnées et le plan d'équation ax+by+cz = N. Ces quantités seront appelées <<surface>> et <<volume>> dans la suite. On verra dans la section suivante qu'on peut calculer S(a,b,N) lorsque a et b sont entiers en temps et en mémoire O(max(lna,lnb)) et donc V(a,b,c,N) en temps O(Nmax(lna,lnb)/c) d'après la relation :
V(a,b,c,N) =
å
0 £ z £ N/c 
S(a,b,N-cz).
Dans l'optique de la recherche du n-ème entier de Hamming, a,b,c seront entiers de l'ordre de 104n et N entier de l'ordre de 104n4/3 donc les calculs de surface se font en temps O(lnn) et les calculs de volume en temps O(n1/3lnn).

Soient a,b,c entiers strictement positifs tels que

b
a
£ ln3
ln2
< b+1
a
,       c
a
£ ln5
ln2
< c+1
a
.
Donc pour tout K :
V æ
è
a,b+1,c+1, Ka
ln2
ö
ø
£ V(ln2,ln3,ln5,K) £ V æ
è
a,b,c, Ka
ln2
ö
ø
car les triangles obliques associés aux deux tétraèdres extrêmes <<encadrent>> le triangle oblique du tétraèdre du milieu. Si ce dernier triangle ne contient aucun point entier, ce qui est le cas entre autres si K n'appartient pas au groupe additif engendré par ln2, ln3 et ln5, alors les trois membres sont égaux pour a suffisamment grand à K fixé donc on peut calculer le volume discret d'un tétraèdre de Hamming donné à condition de disposer d'encadrements suffisamment précis de ln3/ln2 et ln5/ln2. Pour fixer les idées, on choisit K de sorte que V(ln2,ln3,ln5,K) » n soit K » an1/3 avec a = (6ln2ln3ln5)1/3 puis a de sorte que le volume physique compris entre les tétraèdres de paramètres a,b+1,c+1,Ka/ln2 et a,b,c,Ka/ln2 soit de l'ordre de 10-4 (ceci ne garantit pas le fait que cette région ne contienne pas de points entiers mais le rend plausible) ce qui donne après calculs a » 104bK3 avec b = (ln3+ln5)/(6ln23ln25) donc a,b,c sont de l'ordre de 104n et N = Ka/ln2 de l'ordre de 104n4/3 comme annoncé.

Avec a,b,c ainsi choisis on détermine par encadrements successifs deux entiers p et q tels que V(a,b,c,p) £ n < V(a,b,c,q) £ V(a,b,c,p)+k où k est une petite constante (k = 12 dans l'implémentation décrite ci-après), ceci peut être effectué avec O(lnn) calculs de volume donc en temps O(n1/3(lnn)2), puis on vérifie que V(a,b+1,c+1,p) = V(a,b,c,p) et V(a,b+1,c+1,q) = V(a,b,c,q). Si le test est positif alors on est sûr que le n-ème point de Hamming est compris entre les plans d'équation ax+by+cz = p et ax+by+cz = q et que c'est le point de rang n-V(a,b,c,p) dans la liste l des points à coordonnées entières positives compris entre ces deux plans. On constitue l par balayage en z : pour chaque z entier compris entre 0 et ëq/cû on compare les surfaces S(a,b,p-cz) et S(a,b,q-cz) ; la différence entre ces deux nombres est égale au nombre de points de l dont l'exposant de 5 vaut z. Lorsque cette différence est strictement positive on recherche les exposants x et y de ces points par dichotomie, le nombre de points tels que y > y0 est égal à S(a,b,q-cz-by0-b) - S(a,b,p-cz-by0-b) donc calculable en temps logarithmique et une fois y connu les points de l ayant un exposant de 3 égal à y sont ceux d'abscisse x0 = ë(q-cz-by)/aû, x0-1,... Enfin, une fois l connue, il ne reste plus qu'à la classer pour l'ordre induit par la forme linéaire (x,y,z)® xln2+yln3+zln5 (en effectuant des comparaisons approchées avec contrôle de précision) et à extraire l'élément de rang n-V(a,b,c,p), cet élément est le n-ème entier de Hamming.

La complexité de la phase de constitution de l est O(n1/3lnn) pour le balayage en z et O((lnn)2) pour l'identification de chacun de éléments de l à z déterminé, soit au total O(n1/3lnn). On a ainsi trouvé le n-ème entier de Hamming en temps O(n1/3(lnn)2) (en nombre d'opérations entières) et en mémoire O(lnn) (en nombre d'entiers).

Exemple d'exécution pour la recherche de l'entier de Hamming de rang 1018 (voir le listing ici) :

bash$ ./volume 1E -debug
 0.02  0.02 Newton   2805474567985183128243159433216 -> 1000002623563795213
516.90 516.88 Newton   2805472114538014809295748381758 -> 999999999997705613
1067.01 550.11 Newton   2805472114540160427841063461191 -> 1000000000000000014
1595.97 528.96 Newton   2805472114540160414748825911744 -> 1000000000000000000
2112.25 516.28 linéaire 2805472114540160416619145561665 -> 1000000000000000003
2628.43 516.18 contrôle 2805472114540160414748825911744 -> 1000000000000000000
3144.42 515.99 contrôle 2805472114540160416619145561665 -> 1000000000000000003
3660.37 515.95 balayage ...
4713.12 1052.75 terminé

x=625963 y=1360652 z=9874

Les deux premières colones indiquent en secondes le temps total écoulé depuis le début de l'exécution et la différence avec le temps précédent c'est à dire le temps de la phase précédente. La troisième colone indique quelle est la phase en cours et les colones suivantes donnent des détails sur cette phase.

mesures de temps et mémoire :

106 109 1012 1015 1018 n
0.34 3.17 24.62 339.49 4713.12 temps (s)
808 836 1044 1044 1044 mémoire (Ko)

3.5.1  Calcul de surface

Soient a,b Î N avec 0 < a < b et N entier ; on veut calculer

S(a,b,N) = card {(x,y) Î N2 / ax+by £ N}.
Déjà on peut remarquer que S(a,b,N) = 0 si N < 0 et S(a,b,N) = 1+ëN/aû si N < b (triangle de hauteur strictement inférieure à 1). Dans le cas général, on effectue la division euclidienne de b par a, soit b = qa+r, et on divise le triangle à mesurer en deux parties :
T1: x ³ 0,    y ³ 0,    b
q
x + by £ N
T2: y ³ 0,    b
q
x + by > N,    ax+by £ N.

triangle.gif
La surface discrète de T1 est calculable directement :
S(T1) = ëN/bû
å
y = 0 
1+ ê
ë
qN
b
ú
û
-qy = æ
è
1+ ê
ë
N
b
ú
û
ö
ø
æ
è
1+ ê
ë
qN
b
ú
û
- q
2
ê
ë
N
b
ú
û
ö
ø
.

Pour calculer la surface de T2 on effectue le changement de variable t = x+qy soit

T2: y ³ 0,    t > qN
b
,     at+ry £ N
et donc
S(T2) = S æ
è
r,a,N- ê
ë
qN
b
ú
û
-a ö
ø
.
Ceci donne donc une relation de récurrence sur S où les paramètres a,b,N sont transformés en r,a et N¢ < N. Le nombre d'étapes avant d'arriver à un cas de base (hauteur < 1) est au plus égal au nombre d'étapes nécessaires au calcul du pgcd de a et b (sinon on aboutit au calcul de S(0,d,N¢¢) avec N¢¢ > 0 ce qui donnerait une surface infinie) donc est O(max(lna,lnb)). La figure ci-dessous illustre les divisions effectuées dans le cas a = 7,b = 12,N = 100 :

tri712100.gif

Complexité mémoire : le calcul récursif de S(a,b,N) peut être conduit en mémoire constante puisqu'il se réduit à une accumulation. Toutefois, comme on sera amené à calculer de nombreuses surfaces ayant les mêmes paramètres a et b, il est avantageux en termes de temps de calcul de mémoriser la suite des paramètres des formules closes ce qui conduit à un encombrement mémoire en O(max(lna,lnb)).

3.5.2  Calcul de volume

La technique de découpage des triangles vue à la section précédente se transpose mal en dimension 3. On peut découper un tétraèdre en deux parties dont le volume de l'une est calculable par une formule close mais la partie restante n'est pas un tétraèdre, elle peut être décomposée en différence de deux tétraèdres ce qui donne une relation de récurrence compliquée pour laquelle la convergence vers un cas de base n'est pas triviale et en tous cas pas rapide. Expérimentalement, la méthode de découpage décrite ci-dessous donne un temps de calcul de volume en O(n1/2) donc est moins performante que le simple découpage d'un tétraèdre en O(n1/3) tranches dont on calcule les surfaces en temps O(lnn). Toutefois, si on arrête la récursion à une petite profondeur (5 semble optimal) et si on calcule les volumes des tétraèdres obtenus par addition de surfaces on obtient une méthode de calcul de volume environ deux fois plus rapide que l'addition des surfaces de toutes les tranches du tétraèdre original.

Soient a,b,c,N entiers avec 0 < a £ b £ c et T = OABC le tétraèdre limité par les plans de coordonnées et le plan d'équation ax+by+cz = N.

Formule close

Comme en dimension 2, le volume de T est donné par une formule close lorsque les coefficients a,b,c ont des rapports entiers l'un à l'autre. Plus précisément, pour q, q¢ et N entiers :

V(1,q¢,qq¢,N)
=
ëN/qq¢û
å
z = 0 
S(1,q¢,N-qq¢z)
=
ëN/qq¢û
å
z = 0 
(ëN/q¢û-qz+1)(N - qq¢z - q¢
2
(ëN/q¢û- qz) + 1)
=
1
12
(g+1)(g(2g+1)q2q¢-3gq(2N+q¢+2)+ 6b(2(N+1) - (1+b)q¢) + 12 (N+1))
avec b = ëN/q¢û et g = ëN/qq¢û. Lorsque q¢ = 1, cette formule se simplifie en :
V(1,1,q,N) = 1
12
(g+1)(g(2g+1)q2 - 3gq(2N+3)+6(N+1)(N+2)).

Cas symétrique

Si a = b alors T est symétrique par rapport au plan d'équation x = y et peut donc être divisé en deux tétraèdres de même volume en comptant à part la tranche OCI située dans le plan bissecteur ce qui donne la relation :
V(a,a,c,N) = 2V(a,2a,c,N-2a)+S(2a,c,N).

tetra1.gif

Cas asymétrique croisé

Si a < b et s'il existe un sous-multiple de c compris entre a et b : a < c/q < b alors on considère le tétraèdre T¢ = OA¢B¢C¢ limité par le plan d'équation

c
q
x + c
q
y + cz = N
dont le volume est V(T) = V(1,1,q,ëqN/cû) connu. Les faces obliques de T et T¢ se croisent le long du segment IC et :
V(T) = V(T¢) + V(A¢AIC) - V(B¢BIC).

tetra3.gif

A¢AIC est défini par les inéquations
y ³ 0,       z ³ 0,       x+y+qz > qN
c
,       ax+by+cz £ N
soit en posant t = x+y+qz - ë[ qN/ c] û -1
y ³ 0,       z ³ 0,       t ³ 0,      at+(b-a)y+(c-qa)z £ N -a ê
ë
qN
c
ú
û
-a
et donc
V(A¢AIC) = V(a,b-a,c,N -aëqN/cû-a).
Des calculs similaires donnent
V(B¢BIC) = V(b-a,b,qb-c, bëqN/cû- N - 1)
d'où finalement

V(a,b,c,N)
=
V(1,1,q,ëqN/cû)
+
V(a,b-a,c-qa, N - aëqN/cû-a)
-
V(b-a,b,qb-c, bëqN/cû- N).

Cas asymétrique emboîté

Si a < b et s'il n'existe pas de sous-multiple de c séparant a et b alors on considère un sous-multiple c/q < a et le tétraèdre T¢ défini comme dans le cas précédent, mais ici T¢ est inclus dans T. Les prolongements des faces obliques de T et T¢ se coupent le long d'un segment CI et l'on a

V(a,b,c,N)
=
V(1,1,q,ëqN/cû) + V(A¢AIC) - V(B¢BIC)
V(A¢AIC)
=
V(a,b-a,c-qa, N - aëqN/cû-a)
V(B¢BIC)
=
V(b-a,b,c-qb, N + a - bëqN/cû- 2b).

tetra2.gif

Convergence

La quantité max(a,b,c) décroît strictement dans le cas a < b et décroît strictement en deux étapes dans le cas a = b donc la terminaison du calcul est assurée et la profondeur de récursion est majorée par 2c, ce qui donne un temps de calcul en O(22c) avec c de l'ordre de 100n... En pratique le temps de calcul n'est pas aussi catastrophique et le fait de borner la profondeur de récursion puis d'additionner les surfaces des tranches des tétraèdres obtenus permet de maintenir une complexité temporelle O(n1/3lnn).

Chapitre 4
Conclusion

Le meilleur algorithme pour calculer tous les entiers de Hamming jusqu'au n-ème est donc celui de Damien Doligez et il est probablement imbattable : aucun nombre en trop n'est calculé, accélérer les quelques comparaisons qui sont effectuées en utilisant les logarithmes aura un effet négligeable sur le temps global de calcul. Pour rendre justice à Denis Cazor, je signale que son programme initial était rédigé en Pascal, ce qui, après traduction en C donne un temps de calcul de 2.47 secondes sur la machine de test pour le millionième nombre de Hamming. Il bat donc celui de Damien mais ceci uniquement grâce aux meilleures performances du compilateur C (gcc) par rapport au compilateur Ocaml (ocamlopt).

En ce qui concerne le calcul direct du n-ème entier de Hamming la méthode volumétrique est la plus efficace connue à ce jour tant en termes de temps de calcul que de mémoire requise, et permet le calcul du n-ème entier de Hamming en temps raisonnable pour des valeurs de n extrêmement élevées. Cependant la situation n'est pas intellectuellement satisfaisante, on attend logiquement un nouvel algorithme fournissant le n-ème entier de Hamming en temps polylogarithmique. Remarquons qu'il existe un tel algorithme pour le problème de Hamming restreint à deux facteurs premiers puisqu'on peut calculer des surfaces de triangles en temps logarithmique. La technique de division des tétraèdres donne des résultats plutôt décevants en comparaison de ceux obtenus par division de triangles et le calcul du volume d'un tétraèdre semble être un problème nettement plus complexe que son équivalent bidimensionnel. Signalons que ce problème admet une solution logarithmique lorsque les sommets du tétraèdre sont à coordonnées entières (voir L.J. Mordell, Lattice points in a tetrahedron and generalized Dedekind sums, Journal Indian Mathem. Soc. 15 (1951), pp 41-46), mais les tétraèdres de Hamming ou leurs approximations rationnelles ne sont malheureusement pas à sommets entiers.

Chapitre 5
Code des programmes

5.1  Listes paresseuses

(* fichier listp.ml : calcul du n-ème entier de Hamming par liste paresseuse *)

open Lazy open Num

(* liste paresseuse d'entiers de Hamming *) type listp = hd:num; tl:listp Lazy.t

(* n-ème élément d'une liste paresseuse *) let rec nème n liste = if n = 0 then liste.hd else nème (n-1) (force liste.tl)

(* multiplie une liste par p *) let rec mult p liste = hd = liste.hd */ p; tl = lazy(mult p (force liste.tl))

(* fusion sans répétition de listes paresseuses *) let rec (@) a b = let c = compare_num a.hd b.hd in if c < 0 then hd=a.hd; tl=lazy((force a.tl) @ b ) else if c > 0 then hd=b.hd; tl=lazy( a @ (force b.tl)) else hd=a.hd; tl=lazy((force a.tl) @ (force b.tl))

(* calcul *) let _ = let rec h = hd = (Int 1); tl = lazy((mult (Int 2) h) @ (mult (Int 3) h) @ (mult (Int 5) h)) in let n = int_of_string(Sys.argv.(1)) in print_string(string_of_num(nème n h)); print_newline()

           

5.2  Files de priorité

(* fichier prifile.ml : insertion dans une file de priorité *)

(* représentation de 2^a*3^b*5^c, l=logarithme *) type entier = a:int; b:int; c:int; l:float let ln2 = log(2.0) and ln3 = log(3.0) and ln5 = log(5.0)

(* file de priorité = ensemble ordonné *) module Hset = Set.Make( struct type t = entier let compare x y = Pervasives.compare x.l y.l end) open Hset

(* avance de n éléments *) let rec avance n h = let x = min_elt h in if n=0 then x else let h = remove x h in let h = add x with c=x.c+1; l=x.l+.ln5 h in let h = if x.c=0 then add x with b=x.b+1; l=x.l+.ln3 h else h in let h = if x.c=0 x.b=0 then add x with a=x.a+1; l=x.l+.ln2 h else h in avance (n-1) h

(* lance les calculs *) let _ = let h = singleton a=0; b=0; c=0; l=0. in let n = int_of_string(Sys.argv.(1)) in let t = avance n h in Printf.printf "2^ print_newline()

         

5.3  Listes mutables

(* fichier cazor.ml : insertion en milieu de liste *)

(* a,b,c = exposants, l=logarithme *) type listm = a:int; b:int; c:int; l:float; mutable tl:listm let ln2 = log(2.) and ln3 = log(3.) and ln5 = log(5.) let rec nil=a=0; b=0; c=0; l=0.; tl=nil (* fin de liste *)

(* insère la cellule u à la bonne place celle après pointée par x *) (* et retourne le nouveau pointeur d'insertion *) let rec insère u x = if u.l > x.tl.l then insère u x.tl else begin x.tl <- u with tl=x.tl; x.tl end

(* avance de n éléments dans la liste de Hamming *) (* x = dernier double inséré, y = dernier triple, z = dernier quintuple *) let rec avance n (a=a; b=b; c=c; l=l as h) x y z = if n=0 then h else begin let z' = a=a; b=b; c=c+1; l=l+.ln5; tl=nil in z.tl <- z'; let y' = if h.c=0 then insère h with b=b+1; l=l+.ln3 y else y in let x' = if h.c=0 h.b=0 then insère h with a=a+1; l=l+.ln2 x else x in avance (n-1) h.tl x' y' z' end

(* lance les calculs *) let _ = let h = a=0; b=0; c=0; l=0.; tl=nil in let n = int_of_string(Sys.argv.(1)) in let t = avance n h h h h in Printf.printf "2^ print_newline()

           

5.4  Classement sans comparaison

(* fichier nocomp.ml : classement sans comparaison *)

(* Suite de Hamming : a,b,c sont les exposants de 2,3,5 *) type listm = a:int; b:int; c:int; mutable tl:listm let (=.) x y = x.a = y.a x.b = y.b x.c = y.c

(* multiplication et concaténation en tête *) let double x = a=x.a+1; b=x.b; c=x.c; tl=x and triple x = a=x.a; b=x.b+1; c=x.c; tl=x and quintuple x = a=x.a; b=x.b; c=x.c+1; tl=x

(* calcul de minimum (méthode de D. Doligez *) open Num

let valeur(x) = (Int 2)**/(Int x.a) */ (Int 3)**/(Int x.b) */ (Int 5)**/(Int x.c) let min x y = if valeur(x) <=/ valeur(y) then x else y

let dmin x y z = match (y.a>0,z.b>0,x.c>0,y.c>0,x.b>0,z.a>0 ) with (* x<=y y<=z z<=x z<=y y<=x x<=z *) |true, _, _, _, _, true -> x |true, _, true, _, _, _ -> z |true, true, _, _, _, _ -> x |_, true, _, _, true, _ -> y |_, true, true, _, _, _ -> y |_, _, true, true, _, _ -> z |_, _, _, true, true, _ -> z |_, _, _, true, _, true -> x |_, _, _, _, true, true -> y

|true, _, _, _, _, _ -> min x z |_, true, _, _, _, _ -> min x y |_, _, true, _, _, _ -> min y z |_, _, _, true, _, _ -> min x z |_, _, _, _, true, _ -> min y z |_, _, _, _, _, true -> min x y

|_, _, _, _, _, _ -> min x (min y z)

(* allonge la liste de Hamming de n éléments *) let rec allonge n h x y z = if n = 0 then h else begin let t = dmin x y z in h.tl <- t; allonge (n-1) t (if t =. x then double x.tl.tl else x) (if t =. y then triple y.tl.tl else y) (if t =. z then quintuple z.tl.tl else z) end

(* lance les calculs *) let _ = let rec h = a=0; b=0; c=0; tl=h in let n = int_of_string(Sys.argv.(1)) in let t = allonge n h (double h) (triple h) (quintuple h) in Printf.printf "2^ print_newline()

          

5.5  Recherche par comptage

(* fichier comptage.ml: recherche par comptage *)

(* exposants de 2,3,5 et logarithme *) type entier = a:int; b:int; c:int; l:float;; let lg3 = log(3.0)/.log(2.0) and lg5 = log(5.0)/.log(2.0)

(* cherche le plus petit k tel que n(2^k) >= n *) (* retourne n(2^k)-n et k *) let rec cherche n k = if n <= 0 then (-n,k) else begin let w = ref(float(k+1)) and c = ref(n) in while !w >= 0.0 do c := !c - truncate(!w/.lg3) - 1; w := !w -. lg5 done; cherche !c (k+1) end

(* liste triée des points dans la tranche ]2^(k-1),2^k] *) let liste(k) = let res = ref [] in for z=0 to truncate(float(k)/.lg5) do for y=0 to truncate((float(k)-.float(z)*.lg5)/.lg3) do let l = float(y)*.lg3 +. float(z)*.lg5 in let x = truncate(float(k) -. l) in res := a=x; b=y; c=z; l = float(x) +. l :: !res done done; Sort.list (fun u v -> u.l > v.l) !res

(* lance les calculs *) let _ = let n = int_of_string(Sys.argv.(1)) in let (p,k) = cherche n 0 in let t = List.nth (liste k) p in Printf.printf "2^ print_newline()

        

5.6  Comptage et classement simultanés

(* fichier compte-classe.ml : comptage et classement simultanés *)
open Num

type cellule = mutable a:int; b:int; c:int; (* exposants de 2,3,5 *) mutable tiers : cellule; (* lien vers u/3 *) mutable triple : cellule; (* lien vers 3u *) mutable cinquième : cellule; (* lien vers u/5 *) mutable quintuple : cellule; (* lien vers 5u *) mutable précédent : cellule; (* lien arrière dans la suite 2-3-5 *) mutable suivant : cellule; (* lien avant dans la suite 2-3-5 *) mutable suiv35 : cellule (* lien avant dans la suite 3-5 *)

let rec nil = a=0;b=0;c=0; tiers = nil; triple = nil; cinquième = nil; quintuple = nil; précédent = nil; suivant = nil; suiv35 = nil

(* +---------------------------------------------+ | Génération des entiers de Hamming impairs | +---------------------------------------------+ *)

let troisfois x = x with b=x.b+1; suiv35=x and cinqfois x = x with c=x.c+1; suiv35=x

let rec début = a=0;b=0;c=0; tiers = nil; triple = nil; cinquième = nil; quintuple = nil; précédent = début; suivant = début; suiv35 = nil

let h = ref(début) (* dernier élément engendré *) and y = ref(troisfois début) (* prochain triple *) and z = ref(cinqfois début) (* prochain quintuple *)

(* calcul de minimum *) let dmin y z = match (z.b>0, y.c>0) with |true,_ -> y |_, true -> z |_, _ -> if Int(3)**/Int(y.b) */ Int(5)**/Int(y.c) >/ Int(3)**/Int(z.b) */ Int(5)**/Int(z.c) then z else y

(* prolonge la liste de Hamming 3-5 d'un élément *) let avance35() = let t = dmin !y !z in !h.suiv35 <- t; h := t; if t.b = !y.b t.c = !y.c then begin let u = !y.suiv35 in t.tiers <- u; u.triple <- t; y := troisfois u.suiv35 end else t.tiers <- nil; if t.b = !z.b t.c = !z.c then begin let u = !z.suiv35 in t.cinquième <- u; u.quintuple <- t; z := cinqfois u.suiv35 end else t.cinquième <- nil

(* +--------------------------+ | Classement et comptage | +--------------------------+ *)

let p = ref(début) (* pointeur d'insertion dans la tranche *) and a = ref(-1) (* décalage exposant de 2 *) and c1 = ref(0) (* cardinal des tranches précédentes *) and c2 = ref(1) (* cardinal de la tranche courante *)

(* comparaison exacte *) let diff x y = if x>y then (x-y,0) else (0,y-x) let (<<) u v = let (a,a') = diff u.a v.a and (b,b') = diff u.b v.b and (c,c') = diff u.c v.c in Int(2)**/Int(a) */ Int(3)**/Int(b) */ Int(5)**/Int(c) </ Int(2)**/Int(a') */ Int(3)**/Int(b') */ Int(5)**/Int(c')

(* insère un élément après la cellule pointée par q *) let rec place q = !h.suivant <- q.suivant; q.suivant.précédent <- !h; q.suivant <- !h; !h.précédent <- q; p := !h; incr c2; avance35(); avance235()

(* augmente la tranche courante d'un élément tant que possible *) (* lève l'exception Exit quand la tranche est pleine *) and avance235() =

!h.a <- - !a; (* mise à niveau exposant de 2 *)

(* cherche un minorant à partir du tiers *) let min3 = let t = !h.tiers in let u = t.précédent in let v = u.triple in if t == nil or u == début then nil else if u.a = t.a + v.a + !a then v else if u.a > t.a + v.a + !a then raise Exit else nil in

(* cherche un minorant à partir du cinquième *) let min5 = let t = !h.cinquième in let u = t.précédent in let v = u.quintuple in if t == nil or u == début then nil else if u.a = t.a + v.a + !a then v else if u.a > t.a + v.a + !a then raise Exit else nil in

(* cherche un majorant à partir du tiers *) let max3 = let t = !h.tiers in let u = t.suivant in let v = u.triple in if t == nil or t == début then nil else if u.a = t.a + v.a + !a then v else if u.a > t.a + v.a + !a then if début << !h then raise Exit else début else nil in

(* cherche un majorant à partir du cinquième *) let max5 = let t = !h.cinquième in let u = t.suivant in let v = u.quintuple in if t == nil or t == début then nil else if u.a = t.a + v.a + !a then v else if max3 == nil u.a > t.a + v.a + !a then if début << !h then raise Exit else début else nil in

(* si un majorant suit un minorant c'est bon *) if min3 != nil (min3 == max3.précédent or min3 == max5.précédent) then place min3 else if min5 != nil (min5 == max3.précédent or min5 == max5.précédent) then place min5

(* sinon recule à partir d'un majorant s'il y en a un *) else let q = ref(if max3 != nil then max3 else max5) in if !q != nil then begin while !q.précédent != !p !q.précédent != min3 !q.précédent != min5 !h << !q.précédent do q := !q.précédent done; place !q.précédent end

(* dernier recours: avance à partir d'un minorant ou de p *) else begin if min5 != nil then p := min5; if min3 != nil then p := min3; while !p.suivant != max3 !p.suivant != max5 !p.suivant << !h do p := !p.suivant; if !p == début then raise Exit done; place !p end (* fin de avance235 (si, si) *)

(* lance les calculs *) let _ = let n = int_of_string(Sys.argv.(1)) in avance35(); while !c1 <= n do incr a; p := début; (try avance235() with Exit -> ()); c1 := !c1 + !c2; done; let x = ref(début) in for i = !c1-1 downto n+1 do x := !x.précédent done; Printf.printf "2^ print_newline()

   

5.7  Arbres différentiels

(* fichier difftree.ml: n-ème nombre de Hamming par arbres différentiels *)

open Num

(*+-----------------------------------------+ | Structure abstraite de groupe ordonné | +-----------------------------------------+*)

module type Groupe = sig type t (* type de base *) val (++) : t -> t -> t (* addition *) val (--) : t -> t -> t (* soustraction *) val (<<) : t -> t -> bool (* comparaison *) val zéro : t (* constantes *) val ln2 : t val ln3 : t val ln5 : t val string : t -> string (* affichage *) end

(*+------------------------+ | Arbres différentiels | +------------------------+*)

module Difftree(G:Groupe) : sig

type arbre (* type abstrait *) val singleton : G.t -> arbre (* arbre à un élément *) val taille : arbre -> num (* taille d'un arbre *) val inc : G.t -> arbre -> arbre (* translation *) val dec : G.t -> arbre -> arbre (* translation réciproque *) val insère : G.t -> arbre -> arbre (* ajoute un élément *) val découpe : G.t -> arbre -> arbre*arbre (* découpe pr. à un pivot *) val recolle : arbre -> arbre -> arbre (* fusion sans recouvrement *) val kème_forêt : num -> num -> arbre list -> G.t (* recherche par rang *)

end = struct open G

(* cardinal, étiquette, gauche, droite *) type arbre = Vide | Noeud of num * t * arbre * arbre

(* utilitaires *) let singleton a = Noeud(Int 1, a, Vide, Vide) and taille = function Vide -> Int 0 | Noeud(t,_,_,_) -> t and inc x = function Vide -> Vide | Noeud(t,a,g,d) -> Noeud(t,a++x,g,d) and dec x = function Vide -> Vide | Noeud(t,a,g,d) -> Noeud(t,a--x,g,d)

(* insère un élément *) let rec insère x = function | Vide -> singleton(x) | Noeud(t,a,g,d) -> if a << x then Noeud(t+/Int(1), a, g, insère (x--a) d) else Noeud(t+/Int(1), a, insère (x--a) g, d)

(* découpe l'arbre en deux parties séparés par x *) let rec découpe x = function | Vide -> (Vide,Vide) | Noeud(t,a,g,d) -> if a << x then let (d1,d2) = découpe (x--a) d in Noeud(t-/taille(d2), a, g, d1), (inc a d2) else let (g1,g2) = découpe (x--a) g in (inc a g1), Noeud(t-/taille(g1), a, g2, d)

(* retire le plus grand élément d'un arbre *) let rec maximum = function | Vide -> failwith "max(vide)" | Noeud(_,a,g,Vide) -> (a,inc a g) | Noeud(t,a,g,d) -> let (x,d') = maximum(d) in (x++a),Noeud(t-/Int(1), a, g, d')

(* recolle deux arbres sans recouvrement *) let recolle g d = if g = Vide then d else let (a,g') = maximum(g) in Noeud(taille(g)+/taille(d), a, dec a g', dec a d)

(* découpe une forêt en deux forêts séparées par x *) (* et retourne les deux forêts et leurs tailles. *) (* (t1,t2,f1,f2) = résultat partiel pour accumulation *) let rec découpe_forêt x ((t1,t2,f1,f2) as accu) = function | [] -> accu | tête::queue -> let (g,d) = découpe x tête in découpe_forêt x (t1+/taille(g), t2+/taille(d), (if g = Vide then f1 else g::f1), (if d = Vide then f2 else d::f2)) queue

(* extrait un arbre de taille maximale *) let rec max_arbre a f = function | [] -> (a,f) | b::suite -> if taille(b) >/ taille(a) then max_arbre b (a::f) suite else max_arbre a (b::f) suite

(* extrait la k-ème étiquette d'un arbre en partant de la fin *) let rec kème_arbre k = function | Vide -> failwith ärbre vide" | Noeud(t,a,g,d) -> if taille(d) =/ k then (a, inc a g, inc a d) else if taille(d) >/ k then let (x,d1,d2) = kème_arbre k d in (a++x, Noeud(t-/taille(d2)-/Int(1),a,g,d1), inc a d2) else let (x,g1,g2) = kème_arbre (k-/taille(d)-/Int(1)) g in (a++x, inc a g1, Noeud(t-/taille(g1)-/Int(1),a,g2,d))

(* extrait la k-ème étiquette de la forêt f en partant de la fin *) (* t = cardinal de la forêt *) let rec kème_forêt k t f = match f with | [] -> failwith "forêt vide" | arbre::suite -> Printf.printf "k= let (arbre,suite) = max_arbre arbre [] suite in let k' = quo_num (k*/taille(arbre)) t in let (a,g,d) = kème_arbre k' arbre in let (t1,t2,f1,f2) = découpe_forêt a (taille(g), taille(d), (if g = Vide then [] else [g]), (if d = Vide then [] else [d])) suite in if t2 >/ k then kème_forêt k t2 f2 else if t2 =/ k then a else kème_forêt (k-/t2-/Int(1)) t1 f1

end (* Difftree *)

(* +---------------------------+ | n-ème nombre de Hamming | +---------------------------+ *)

module Hamming(G:Groupe) : sig

val hamming : num -> G.t

end = struct

module D = Difftree(G) open G open D

type tétraèdre = mutable m2: t; (* multiple de ln2 *) mutable m5: t; (* multiple suivant de ln5 *) mutable n : num; (* taille tétraèdre *) mutable p : num; (* taille triangle *) mutable l : arbre; (* bande d'épaisseur ln3 *) mutable t : arbre list (* tranche d'épaisseur ln2 *)

(* retourne le kème nombre de Hamming *) let hamming k =

let t = m2=zéro; m5=ln5; n=Int(0); p=Int(0); l=singleton(zéro); t=[] in while t.n </ k do

let m2 = t.m2 in t.m2 <- t.m2 ++ ln2; let (g,d) = découpe t.m2 t.l in t.t <- dec m2 g :: t.t; t.l <- recolle d (inc ln3 g); t.p <- t.p +/ taille(g); t.n <- t.n +/ t.p; if t.m5 << t.m2++ln3 then (t.l <- insère t.m5 t.l; t.m5 <- t.m5 ++ ln5)

done;

if t.n =/ k then t.m2 else let a = kème_forêt (t.n-/k-/Int(1)) t.p t.t in a ++ t.m2 -- ln2

end (* Hamming *)

(* +----------------------+ | entiers de Hamming | +----------------------+ *)

module Hnum : Groupe = struct

type t = int * int * int (* exposants de 2 3 5 *) let zéro = (0, 0, 0) and ln2 = (1, 0, 0) and ln3 = (0, 1, 0) and ln5 = (0, 0, 1) and (++) (a,b,c) (x,y,z) = (a+x,b+y,c+z) and (--) (a,b,c) (x,y,z) = (a-x,b-y,c-z) and string(a,b,c) = Printf.sprintf "2^

(* approximations par défaut à 14 décimales des logarithmes en base 2 *) let lg2 = num_of_string "100000000000000" and lg3 = num_of_string "158496250072115" and lg5 = num_of_string "232192809488736"

(* comparaison approchée puis exacte en cas de doute *) let (<<) (a,b,c) (x,y,z) = let a = a-x and b = b-y and c = c-z in let m = abs(b) + abs(c) and l = lg2*/Int(a) +/ lg3*/Int(b) +/ lg5*/Int(c) in if l </ Int(-m) then true else if l >/ Int(m) then false else begin Printf.fprintf stderr "précision insuffisante a= Int(2)**/Int(a) */ Int(3)**/Int(b) */ Int(5)**/Int(c) </ Int(1) end

end (* Hnum *)

(* +---------------------+ | Lance les calculs | +---------------------+ *)

(* conversion string -> num avec suffixe *) let num_of_string(s) = let l = String.length(s) in let (e,i) = match s.[l-1] with |('0'..'9') -> 0, l | 'K' -> 3, l-1 | 'M' -> 6, l-1 | 'G' -> 9, l-1 | 'T' -> 12, l-1 | 'P' -> 15, l-1 | 'E' -> 18, l-1 | 'Z' -> 21, l-1 | 'Y' -> 24, l-1 |_ -> failwith ßuffixe inconnu" in Num.num_of_string(String.sub s 0 i) */ (Int 10)**/(Int e)

module H = Hamming(Hnum) let _ = let k = num_of_string(Sys.argv.(1)) in let a = H.hamming(k) in print_endline(Hnum.string a)

        

5.8  Méthode volumétrique

(*+--------------------------------------------------------------------------+
  |                                                                          |

| Calcul du n-ème nombre de Hamming | | |

| Méthode volumétrique | | |

+--------------------------------------------------------------------------+*)

(* Michel Quercia, le 08/06/99 *) (* compilation : ocamlopt -o hamming nums.cmxa unix.cmxa volume.ml -cclib -lnums -cclib -lunix *)

(*+-------------------------+ | Messages de déboguage | +-------------------------+*)

let debug = ref false (* option -debug *) open Printf open Unix

let t0 = ref(0.0) (* dernier relevé chrono *)

let message texte = if !debug then begin let t1 = (let t = times() in t.tms_utime +. t.tms_stime) in fprintf Pervasives.stderr " t0 := t1; flush Pervasives.stderr end

let mesg texte = if !debug then begin prerr_string(texte()); flush Pervasives.stderr end

(*+--------------------------------------------------------------------------+ | |

| Descriptions abstraites | | |

+--------------------------------------------------------------------------+*)

(* +-------------------+ | nombres entiers | +-------------------+ *)

module type Entiers = sig type t

val ( ++ ) : t -> t -> t (* opérations usuelles *) val ( -- ) : t -> t -> t val ( ** ) : t -> t -> t val ( // ) : t -> t -> t val (<<= ) : t -> t -> bool val ( == ) : t -> t -> bool

val zéro : t (* on aura besoin de tous ceux là *) val un : t val deux : t val trois : t val six : t val douze : t

val of_string : string -> t (* fonctions de conversion *) val string_of : t -> string

val cubroot : t -> t (* racine cubique approximative *) end

(* +---------------------+ | Calcul de surface | +---------------------+ *)

module type Surf = functor (Z:Entiers) -> sig open Z type coef (* coefficients a,b *) val make : t * t -> coef (* enregistre a,b *) val surface : coef -> t -> t (* surface ax+by <= n *)

end

(* +--------------------+ | Calcul de volume | +--------------------+ *)

module type Vol = functor (Z:Entiers) -> sig open Z type coef (* coefficients a,b,c, hmax et pmax *) val make : t * t * t * t * int -> coef (* enregistre a,b,c,h,p *) val volume : coef -> t -> t (* volume ax+by+cz <= n *) end

(* +-------------------------------------------------+ | Recherche d'un entier de Hamming ou d'un rang | +-------------------------------------------------+ *)

module type Ham = functor (Z:Entiers) -> sig open Z val hamming : t -> int -> t -> t -> t -> t -> unit val rang : t -> int -> t -> t -> t -> t -> t -> t -> unit

(* hamming hmax pmax a b c k *) (* rang hmax pmax a b c x y z *) end

(* +------------------------------------------------------------------------+ | |

| Implémentation des entiers | | |

+------------------------------------------------------------------------+ *)

(* entiers de longueur arbitraire *) module Znum = struct open Num type t = num let ( ++ ) = ( +/ ) and ( -- ) = ( -/ ) and ( ** ) = ( */ ) and ( // ) = quo_num and (<<= ) = (<=/ ) and ( == ) = ( =/ ) and zéro = Int(0) and un = Int(1) and deux = Int(2) and trois = Int(3) and six = Int(6) and douze = Int(12) and of_string = num_of_string and string_of = string_of_num and cubroot x = let y = floor(float_of_num(x)**(1.0/.3.0)) in num_of_string(Printf.sprintf "end

(* flottants = entiers de 64 bits *) module Zfloat = struct open Pervasives type t = float let ( ++ ) = ( +. ) and ( -- ) = ( -. ) and ( ** ) = ( *. ) and ( // ) = fun x y -> floor(x/.y) and (<<= ) = (<= ) and ( == ) = ( = ) and zéro = 0. and un = 1. and deux = 2. and trois = 3. and six = 6. and douze = 12. and of_string = float_of_string and string_of x = Printf.sprintf " and cubroot x = floor(x**(1.0/.3.0)) end

(*+--------------------------------------------------------------------------+ | |

| Surface d'un triangle | | |

+--------------------------------------------------------------------------+*)

module Surface:Surf = functor(Z:Entiers) -> struct open Z

(* a,b = coefficients du triangle. *) (* q = b/a *) (* next = triangle suivant s'il a été déjà visité *) type coef = a:t; b:t; q:t; next:coef Lazy.t

(* enregistre a,b *) let rec make(u,v) = let (a,b) = if u <<= v then (u,v) else (v,u) in let rec c = a=u; b=v; q=v//u; next=lazy(prolonge c) in c

and prolonge (a=a; b=b; q=q as coef) = let r = b -- q**a in make(r,a)

(* calcul récursif de surface *) let rec surf a=a; b=b; q=q; next=next n s = if b <<= n then (* hauteur >= 1 *) let nb = n//b and qnb = (n**q)//b in surf (Lazy.force next) (n -- a**(qnb ++ un)) (s ++ ((nb ++ un)**(deux**qnb -- q**nb ++ deux))//deux) else if zéro <<= n then s ++ (n // a ++ un) (* hauteur < 1 *) else s

let surface coef n = surf coef n zéro

end (* Surface *)

(*+--------------------------------------------------------------------------+ | |

| Volume d'un tétraèdre | | |

+--------------------------------------------------------------------------+*)

module Volume:Vol = functor (Z:Entiers) -> struct

module S = Surface(Z) open Z

(* a,b,c = coefficients du tétraèdre *) (* p = profondeur maximale *) (* h = hauteur additions brutales *) (* next = détail des calculs selon les cas *) type coef = a:t; b:t; c:t; h:t; p:int; next:calcul Lazy.t and calcul = | Clos of t (* formule close *) | Sym of coef * S.coef (* a=b, 1/2-tétr. et tranche *) | Asym1 of t * coef * coef * S.coef (* cas emboité *) | Asym2 of t * coef * coef * S.coef (* cas croisé *) (* q tét+ tét- face(a,b) *)

(* enregistre les coefficients *) let rec make(u,v,w,h,p) = let (u,v) = if v<<=u then (v,u) else (u,v) in let (a,b,c) = if v<<=w then (u,v,w) else if u<<=w then (u,w,v) else (w,u,v) in let rec coef = a=a; b=b; c=c; h=h; p=p; next=lazy(prolonge coef) in coef

and prolonge (a=a; b=b; c=c; h=h; p=p as coef) = if a == b then let q = c//a in if c == q**a then Clos(q) else Sym(make(a,deux**a,c,h,p), S.make(deux**a,c))

else let q = (c++b--un)//b in if q**a <<= c then Asym2(q, make(a,b--a,c--q**a,h,p-1), make(b--a,b,q**b--c,h,p-1), S.make(a,b))

else let q = q--un in Asym1(q, make(a,b--a,c--q**a,h,p-1), make(b--a,b,c--q**b,h,p-1), S.make(a,b))

(* formule close V(1,1,q,N) *) let v(q,n) = let r = n//q in let num = r++un in let den = r**(deux**r++un)**q**q -- trois**r**q**(deux**n++trois) ++ six**(n++un)**(n++deux) in (num ** den)//douze

(* addition des surfaces horizontales *) let rec addition coef c n v = if zéro <<= n then addition coef c (n--c) (v ++ S.surface coef n) else v

(* calcul récursif du volume, addition de surfaces si profondeur = 0 ou hauteur <= hmax *) let rec volume (a=a; b=b; c=c; h=h; p=p; next=next as coef) n =

if b <<= n then match Lazy.force next with (* hauteur >= 1 *) | Clos(q) -> v(q,n//a) | Sym(c,t) -> deux**(volume c (n--a)) ++ (S.surface t n)

| Asym1(q,g,d,t) -> if h**c <<= n p <> 0 then let qnc = (q**n)//c in v(q,qnc) ++ (volume g (n -- a**qnc --a)) -- (volume d (n ++ a -- b**(qnc++deux))) else addition t c n zéro

| Asym2(q,g,d,t) -> if h**c <<= n p <> 0 then let qnc = (q**n)//c in v(q,qnc) ++ (volume g (n -- a**qnc -- a)) -- (volume d (b**qnc -- n -- un)) else addition t c n zéro

else if zéro <<= n then (n//a)++un else zéro (* h < 1 *)

end (* Volume *)

(*+--------------------------------------------------------------------------+ | |

| Recherche d'un entier de Hamming ou d'un rang | | |

+--------------------------------------------------------------------------+*)

module Hamming:Ham = functor (Z:Entiers) -> struct

module S = Surface(Z) module V = Volume(Z) open Z open S open V

(* vérifie un volume *) let check coef n v =

message(fun _ -> sprintf "contrôle let k = volume coef n in mesg(fun _ -> sprintf " if not(k==v) then failwith "contrôle négatif"

(* tri selon l'ordre de Hamming *) let cmp a b c (x,y,z) (x',y',z') = let d = a**(x--x') ++ b**(y--y') ++ c**(z--z') in if d ++ y ++ z <<= zéro then true else if zéro <<= d -- y' -- z' then false else failwith "comparaison douteuse"

let nth a b c l n = let rec loop n l = if n == zéro then List.hd l else loop (n--un) (List.tl l) in loop n (Sort.list (cmp a b c) l)

(*+--------------------------------------+ | Encadre le k-ème nombre de Hamming | +--------------------------------------+*)

let recherche hmax pmax a b c k =

let deuxabc = deux ** a ** b ** c in let sixabc = trois ** deuxabc in let coef = V.make(a,b,c,hmax,pmax) in

(* applique la méthode de Newton tant que l'encadrement se resserre *) (* hypothèse : nmin <= n < nmax, vmin <= k < vmax *) let rec newton(nmin,vmin,nmax,vmax,n,k) = if vmax -- vmin <<= douze then (nmin,vmin,nmax,vmax) else begin

message(fun _ -> sprintf "Newton let v = volume coef n in mesg(fun _ -> sprintf " if v == k then (n,v,nmax,vmax) else let n' = n -- (deuxabc**(v--k))//(n**n) in if v <<= k

then if nmax <<= n' or v -- vmin <<= douze or vmax -- v <<= douze then linéaire(n,v,nmax,vmax,k) else newton(n,v,nmax,vmax,n',k)

else if n' <<= nmin or v -- vmin <<= douze or vmax -- v <<= douze then linéaire(nmin,vmin,n,v,k) else newton(nmin,vmin,n,v,n',k)

end

(* termine la recherche par interpolation linéaire *) and linéaire(nmin,vmin,nmax,vmax,k) = if vmax -- vmin <<= douze then (nmin,vmin,nmax,vmax) else begin

let n = nmax -- ((vmax--k)**(nmax--nmin))//(vmax--vmin) in message(fun _ -> sprintf "linéaire let v = volume coef n in mesg(fun _ -> sprintf " if v == k then (n,v,nmax,vmax) else if v <<= k then linéaire(n,v,nmax,vmax,k) else linéaire(nmin,vmin,n,v,k) end in

let n0 = cubroot(k**sixabc) in let (p,u,q,v) = newton(zéro,zéro,n0++un,k++a++b++c,n0,k) in if v -- u <<= douze then (p,u,q,v) else let (_,_,q,v) = linéaire(p,u,q,v,k++deux) in (p,u,q,v)

(* +--------------------------------------------------+ | Liste tous les points compris entre deux plans | +--------------------------------------------------+ *)

let liste a b c p q d =

message(fun _ -> "balayage "); let surf = surface (S.make(a,b)) in

(* empile d points à y et z constants *) let rec ligne x y z d l = mesg(fun _ -> "."); if d == un then (x,y,z)::l else ligne (x--un) y z (d--un) ((x,y,z)::l) in

(* empile d points à z constant, origine en (x,y) *) let rec triangle x y z p q d l = if not(b <<= q) then ligne (x++q//a) y z d l else let t = p//(deux**b) in let r = b**(t++un) in let e = surf(q--r) -- surf(p--r) in let (d,l) = if e == zéro then (d,l) else (d--e,triangle x (y++t++un) z (p--r) (q--r) e l) in if d == zéro then l else let t = p//(deux**a) in let r = a**(t++un) in triangle (x++t++un) y z (p--r) (q--r) d l in

(* empile d points à partir de la cote z *) let rec tétraèdre z p q d l = let e = surf(q) -- surf(p) in let (d,l) = if e == zéro then (d,l) else (d--e,triangle zéro zéro z p q e l) in if d == zéro then l else tétraèdre (z++un) (p--c) (q--c) d l in

let l = tétraèdre zéro p q d [] in mesg(fun _ -> ""); l

(*+--------------------------------------+ | Calcule le k-ème nombre de Hamming | +--------------------------------------+*)

let hamming hmax pmax a b c k =

let (p,u,q,v) = recherche hmax pmax a b c k in let coef = V.make(a,b++un,c++un,hmax,pmax) in check coef p u; check coef q v; let l = liste a b c p q (v--u) in let (x,y,z) = nth a b c l (k--u) in message(fun _ -> "terminé"); printf "x=

(*+------------------------------------------+ | Calcule le rang d'un nombre de Hamming | +------------------------------------------+*)

let rang hmax pmax a b c x y z =

let n = x**a ++ y**b ++ z**c in let epsilon = n//b ++ un in let nmin = n -- epsilon and nmax = n ++ epsilon in let coef = V.make(a,b,c,hmax,pmax) and coef' = V.make(a,b++un,c++un,hmax,pmax) in

(* calcule les volumes supérieur et inférieur *) let vol(n) = message(fun _ -> sprintf "plan sup let k = volume coef n in mesg(fun _ -> sprintf " check coef' n k; k in let rmin = vol(nmin) and rmax = vol(nmax) in message(fun _ -> "terminé"); if rmax == rmin ++ un then printf "r= else printf "

end (* Hamming *)

(* +------------------------------------------------------------------------+ | |

| Interface ligne de commande | | |

+------------------------------------------------------------------------+ *)

module Frontend(Z:Entiers) = struct

module H = Hamming(Z) open Z open H open String

(* Valeurs approchées de ln(2), ln(3), ln(5). Avec 50 chiffres on a de quoi voir venir *) let ln2 = "10000000000000000000000000000000000000000000000000" and ln3 = "15849625007211561814537389439478165087598144076924" and ln5 = "23219280948873623478703194294893901758648313930245"

(* décode un nombre *) let int s = let l = length(s) in if l = 0 then zéro else let (e,i) = match s.[l-1] with |('0'..'9') -> 0, l | 'K' -> 3, l-1 | 'M' -> 6, l-1 | 'G' -> 9, l-1 | 'T' -> 12, l-1 | 'P' -> 15, l-1 | 'E' -> 18, l-1 | 'Z' -> 21, l-1 | 'Y' -> 24, l-1 |_ -> failwith ßuffixe inconnu" in of_string ((sub s 0 i) ^ (make e '0'))

let main hmax pmax x y z = (* si z est défini on veut un rang. Sinon on cherche le y-ème nb *) (* de Hamming. Bidouillage (infâme ?) pour avoir le -n facultatif. *)

let rg = z <> "" in let hmax = int(hmax) and x = int(x) and y = int(y) and z = int(z) in let l = length((string_of x) ^ (string_of y) ^ (string_of z)) + 4 in let a = of_string(sub ln2 0 l) and b = of_string(sub ln3 0 l) and c = of_string(sub ln5 0 l) in

if rg then rang hmax pmax a b c x y z else hamming hmax pmax a b c y

end (* Frontend *)

(* lance les calculs *) open Arg

let x = ref "" and y = ref "" and z = ref "" and float = ref false and hmax = ref "1000" and pmax = ref(5)

let helpmsg = üsage : " ^ Sys.argv.(0) ^ " (-r x y z | -n n) [-h hmax] [-p prof] [-debug] ( -float | -num)] Par défaut : -n, -h 1000, -p 5. " let _ = parse [ ("-r", String(fun s -> x := s), " cherche un rang"); ("-n", String(fun s -> y := s), " cherche un nombre"); ("-h", String(fun s -> hmax := s), " hauteur seuil"); ("-p", Int (fun p -> pmax := p), " profondeur maxi"); ("-debug", Set(debug), " débogage"); ("-float", Set(float), " entier = flottant"); ("-num", Clear(float), " entier = num") ] (fun s -> if !y = "" then y := s else z := s) helpmsg

module Fnum = Frontend(Znum) module Ffloat = Frontend(Zfloat)

let _ = if !y <> "" then (if !float then Ffloat.main else Fnum.main) !hmax !pmax !x !y !z else prerr_endline helpmsg

          

Table des matières

1  Introduction
2  Calcul des n premiers entiers de Hamming
    2.1  Calcul paresseux
    2.2  Files de priorité
        2.2.1  Ensembles ordonnés
        2.2.2  Listes mutables
    2.3  Classement sans comparaison
3  Méthodes géométriques
    3.1  Tétraèdres de Hamming
    3.2  Recherche par comptage
    3.3  Comptage et classement simultanés
    3.4  Arbres différentiels
    3.5  Méthode volumétrique
        3.5.1  Calcul de surface
        3.5.2  Calcul de volume
4  Conclusion
5  Code des programmes
    5.1  Listes paresseuses
    5.2  Files de priorité
    5.3  Listes mutables
    5.4  Classement sans comparaison
    5.5  Recherche par comptage
    5.6  Comptage et classement simultanés
    5.7  Arbres différentiels
    5.8  Méthode volumétrique


File translated from TEX by TTH, version 2.27.
On 16 Jun 1999, 18:24.