Chapitre 3
Quelques notions d’algorithmique
Le chapitre 2 a introduit susamment d’éléments du langage C pour permettre de rédiger
des programmes simples, c’est-à-dire exprimer des algorithmes dans un formalisme qui permet
à un ordinateur de les exécuter.
Nous nous intéressons à présent à la façon de concevoir de tels algorithmes, c’est-à-dire aux
techniques qui permettent de passer de l’énoncé d’un problème informatique à un algorithme ré-
solvant ce problème. Il n’existe cependant pas de méthode systématique permettant de faire cela;
l’algorithmique ne peut s’apprendre qu’en se construisant une expérience basée sur la résolution
d’exercices variés. Dans ce chapitre, nous allons introduire un problème particulier, la recherche
de nombres parfaits, et en développer plusieurs solutions de plus en plus élaborées. Nous étu-
dierons ensuite des mécanismes permettant de comparer les performances de ces solutions, et de
s’assurer qu’elles sont correctes.
3.1 La recherche de nombres parfaits
Un nombre entier n1 est dit parfait s’il est égal à la somme de ses diviseurs, excepté
lui-même. Par exemple, le nombre 28 est parfait, car on a
28 =1+2+4+7+14.
Le problème que nous allons chercher à résoudre est celui qui consiste à trouver tous les
nombres parfaits qui appartiennent à un intervalle donné, par exemple ceux qui sont inférieurs à
106.
57
n<1000000
faux
n1
Déterminer si n
est parfait
nn+1
vrai
Figure 3.1 – Énumération des nombres à tester
3.1.1 Première solution
Lorsqu’on est confronté à un problème algorithmique, une bonne stratégie consiste à essayer
de le décomposer en une combinaison de sous-problèmes plus simples. Dans le cas présent, le
problème de rechercher tous les nombres parfaits inférieurs à 106peut se décomposer en deux
sous-problèmes :
Énumérer tous les entiers dans l’intervalle [1,1061].
Pour chacun de ces entiers, déterminer s’il est parfait ou non.
Le premier de ces sous-problèmes ne présente aucune diculté; nous avons vu au chapitre 2
comment programmer une boucle qui énumère toutes les valeurs dans un intervalle donné. Une
solution possible est donnée par l’organigramme de la figure 3.1 : on initialise une variable nà
1, et on l’incrémente tant que le gardien de boucle n<106reste vrai.
Nous avons donc réduit le problème à celui de déterminer si un nombre n1 donné est
parfait ou non.
Une façon simple de résoudre ce dernier problème consiste à se baser sur la définition d’un
nombre parfait : pour déterminer si un nombre n1 est parfait, il sut de calculer la somme
mde tous les diviseurs de n(sauf nlui-même), et de déterminer si mest égal à n. Pour calculer
58
d<n
faux
dd+1
d1
m0
ddivise n
mm+d
m=n
nest parfait parfait
nn’est pas
vrai
faux
vrai
faux
vrai
Figure 3.2 – Organigramme de la première solution
m, on peut énumérer tous les diviseurs potentiels dde ndiérents de n, qui appartiennent néces-
sairement à l’intervalle [1,n1], tester pour chacun d’entre eux s’il divise eectivement n, et
additionner ceux qui satisfont cette propriété.
Un organigramme formalisant cette solution est donné à la figure 3.2. Sa traduction en lan-
gage C est donnée à la figure 3.3.
Remarquons que dans ce programme, la borne supérieure des entiers à énumérer est représen-
tée par une constante n_max, plutôt que d’être directement encodée dans l’expression du gardien
de boucle. Il s’agit d’une bonne habitude de programmation, qui permet de facilement modi-
fier la valeur de cette borne supérieure, en garantissant que cette modification sera correctement
répercutée vers tous les endroits du programme qui en dépendent.
3.1.2 Deuxième solution
Il est clair que l’algorithme de la figure 3.2 n’est pas optimal. En eet, cet algorithme eectue
un certain nombre d’opérations qui sont inutiles. Par exemple, il n’est pas nécessaire de tester si 1
divise n, car cette propriété est toujours vraie. On pourrait donc gagner une étape en énumérant les
diviseurs potentiels dde nà partir de 2 plutôt que de 1. Cela nécessiterait d’initialiser la somme m
des diviseurs à 1 plutôt qu’à 0, ce qui revient à considérer que le diviseur 1 est systématiquement
présent. Cela n’est cependant pas le cas pour le nombre n=1, car on se limite aux diviseurs qui
sont strictement inférieurs à n. Ce problème peut être résolu en commençant l’énumération des
nombres nà considérer à partir de 2 plutôt que de 1, ce qui est correct car 1 n’est pas un nombre
59
#include <stdio.h>
int main()
{
const unsigned n_max = 999999;
unsigned n, m, d;
for (n = 1; n <= n_max; n++)
{
for (d = 1, m = 0; d < n; d++)
if (!(n % d))
m += d;
if (m == n)
printf("%d\n", n);
}
}
Figure 3.3 – Implémentation de la première solution
parfait.
Une autre amélioration est de réaliser que le plus grand diviseur de ninférieur à nest au plus
égal à n/2, ce qui permet de réduire l’intervalle dans lequel on recherche les diviseurs potentiels
à [2,bn/2c]. Enfin, lorsque l’on additionne les diviseurs qui ont été trouvés, si l’on obtient une
valeur intermédiaire mqui est strictement supérieure à n, il n’est pas nécessaire de continuer à
chercher d’autres diviseurs de n. On sait en eet déjà que nn’est pas parfait, car la somme de
tous ses diviseurs (sauf lui-même) est alors forcément supérieure à n.
Une implémentation de toutes ces améliorations est donnée à la figure 3.4. Dans ce pro-
gramme, il y a deux façons de sortir de la boucle sur d: soit on a d>n/2 et mcontient la
somme de tous les diviseurs de ninférieurs à n, soit on a m>net mcontient la somme d’un
sous-ensemble des diviseurs de n. Dans les deux cas, nest un nombre parfait si et seulement si
l’on a m=n.
3.1.3 Troisième solution
Pour encore améliorer la solution fournie par le programme de la figure 3.4, il faut étudier
plus en profondeur le problème que l’on cherche à résoudre. On peut remarquer que pour tout
nombre n1 et diviseur dde n, le nombre d0=n/dest également un diviseur de n.
En eet, les diviseurs de npeuvent être regroupés en paires, les diviseurs det d0étant appariés
60
#include <stdio.h>
int main()
{
const unsigned n_max = 999999;
unsigned n, m, d;
for (n = 2; n <= n_max; n++)
{
for(d=2,m=1;d<=n/2&&m<=n;d++)
if (!(n % d))
m += d;
if (m == n)
printf("%d\n", n);
}
}
Figure 3.4 – Implémentation de la deuxième solution
si l’on a d.d0=n. Ce mécanisme est illustré par le diagramme suivant pour n=24 :
2412864321
On peut exploiter cette propriété pour énumérer plus ecacement les diviseurs de nqui sont
supérieurs à 1 et inférieurs à n: au lieu de balayer l’intervalle [2,bn/2c], il sut d’énumérer
toutes les valeurs de ddans l’intervalle [2,bnc], et pour chacune d’entre elles, de considérer
les deux candidats diviseurs det n/d. L’avantage est que le nombre d’étapes nécessaires pour
trouver l’ensemble des diviseurs de nest alors considérablement réduit pour de grandes valeurs
de n.
Il y a cependant un cas particulier qu’il faut veiller à traiter correctement : si nest un carré
parfait, c’est-à-dire, s’il existe kNtel que n=k2, alors le diviseur d=kest tel que d0=n/d=
d, en d’autres termes ce diviseur se retrouve apparié avec lui-même.
61
1 / 31 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !