Vous êtes sur la page 1sur 10

Algorithmique algébrique

par
Daouda Niang Diatta

Résumé

Ce chapitre est un chapitre d'introduction : faire des opérations exactes sur un ordinateur,
en précision innie, ne peut se faire sans précaution. Les limites imposées par la machine,
principalement la taille de la mémoire disponible et la vitesse d'exécution des programmes,
se font rapidement sentir. On ne peut donc aborder l'algorithmique algébrique sans avoir
au moins un minimum de repères informatiques, notamment la notion de complexité des
algorithmes. C'est le sujet que nous allons brièvement développer ici.

1 Calcul formel : quelques généralités


Le calcul formel a commencé à se développer vers les années 1970, lorsque les ordinateurs devinrent
assez puissants pour envisager des calculs polynomiaux, des dérivations itérées de grosses fonctions
ou bien des calculs de primitives pratiquement inaccessibles à la main. Ce genre de calcul est
évidemment purement mécanique et il semble naturel qu'un ordinateur puisse réaliser ce travail
automatiquement. On obtenait alors la valeur formelle d'une fonction dérivée ou d'une primitive
et non pas simplement une valeur approchée en un point particulier. On pouvait donc manipuler
des formules avec un ordinateur ! Avec la puissance accrue des machines et le développement de
nouveaux algorithmes, de nombreux systèmes formels seront développés depuis, capable de mener
des calculs de plus en plus gros sur des objets de plus en plus abstraits.

1.1 Calcul formel et calcul numérique

La première caractéristique de tels systèmes est de pouvoir faire tout calcul exactement et donc
d'appréhender les objets pour ce qu'ils sont : ainsi le rationnel 1 / 3 ne peut être en aucun cas
remplacé dans un système formel par un nombre décimal du genre 0,333333 qui est un autre
rationnel qui ne lui est pas égal !

On doit impérativement faire travailler la machine en précision innie. Tout calcul sera exécuté
sans aucun recours à des calculs approchés. Ceci présente avantage et inconvénient.

D'une part, il n'y a aucune erreur dans l'évaluation d'une formule, et en cela, on est débarrassé du
problème auquel on est constamment confronté en analyse numérique, les erreurs d'approximation
et incertitudes numériques.

Mais d'un autre coté, il faut être prêt à gérer des nombres de plus en plus gros, qui prendront pour
être stockés de plus en plus de place mémoire, et sur lesquels les opérations seront de plus en plus
longues à être exécutées.

Prenons un exemple simple pour illustrer les divers problèmes qui se posent en calcul numérique et
1
formel. Si nous cherchons la valeur du polynôme x7 ¡ 5x + 1 en 71000 . Un bon système numérique
nous donne 1, ce qui est manifestement faux mais reste cependant un nombre simple, facile à
manipuler et proche du résultat exact. Quant à un système formel, il calcule la valeur exacte qui est
une fraction dont le numérateur aussi bien que le dénominateur ont 5916 chires chacun en base 10.

L'exactitude autant que la rapidité ont donc chacun un prix ! On peut résumer les deux approches
dans le tableau ci-dessous

1
Calcul numérique Calcul formel
approximation en virgule ottante des nombres nombres exactement représentés
erreur d'approximation résultats exacts
calculs rapides calculs lents
encombrement mémoire réduit grosse capacité mémoire requise

En fait, de plus en plus, ces deux philosophies tendent à se compléter l'une l'autre plutôt qu'à
s'opposer : des parties particulièrement instables peuvent être traitées formellement, le calcul
numérique reprenant la main pour accélérer les calculs posant moins de problème.

Le souci du mathématicien qui veut réaliser des calculs formellement sera de proposer des algo-
rithmes aussi rapides que possible en faisant attention à la taille des résultats générés au cours du
calcul, de façon à minimiser simultanément l'encombrement en mémoire et la durée des calculs, les
deux contraintes étant liées par le fait que plus les données deviennent grosses plus leur manipu-
lation prend de temps.

Evidemment ces soucis n'existent pas en calcul numérique : toute opération en virgule ottante
prend le même temps de calcul, que les nombres soient petits ou gros, et le stockage du résultat
nécessite toujours le même nombre d'octets. Cette problématique, particulière au calcul formel,
reviendra constamment tout au long de ce cours.

1.2 Quelques points délicats

Deux remarques vont surprendre celui qui découvre pour la première fois un système de calcul
formel : la place des nombres réels et les les dicultés de manipulation et de présentation des
résultats.

La place des nombres réels : ils sont omniprésents dans presque toutes les mathématiques
et leurs applications; cependant il n'y a pas plus insaisissable qu'un nombre réel. Les seuls réels
immédiatement codable, i.e inscriptibles en mémoire d'ordinateur, sont les décimaux.

Comment faire dans le cadre du calcul formel, lorsque l'on prétend dire des choses exactes ? En
réalité, le problème est dicile. Une classe de nombres réels que l'on appelle nombres algébriques,
peut être manipulée relativement simplement.

Dénition 1. (Nombres algébriques) Un nombre 2 C est dit algébrique, s'il existe un


polynôme non nul P 2 Z[X] tel que P ( ) = 0. Un nombre non algébrique est dit transcendant.

Remarque. L'ensemble des nombres algébriques est un sous-corps de C, contenant Q mais ne


 car c'est le plus petit corps algébriquement clos contenant
contenant pas R. On le note souvent Q
Q.

L'ensemble des nombres algébriques est dénombrable, donc négligeable, puisque les polynômes non
nuls à coecients entiers sont dénombrables et que chacun d'eux possède un nombre ni de zéros.

Toutefois la quasi-totalité des nombres réels sont transcendants; aussi pour un système de calcul
formel leurs existence est-elle fantomatique !

Comment alors manipuler des nombres transcendants tels que p ou e ?

2
Il faudra pour chacun d'eux fournir au système des relations caractéristiques en nombre susant
qui permettront à la machine de manipuler le nombre. Evidemment, ceci ne peut se faire qu'au
cas par cas.

Par exemple la constante e est la valeur en 1 de la fonction exponentielle, laquelle est dénie comme
la solution de l'équation diérentielle Y 0 = Y avec Y (0) = 1 !

Quant à , le fait que ei = ¡1 est fondamental et permet, via la formule de Moivre, de donner la
valeur exacte des fonctions trigonométriques en les multiples rationnels de .

Les dicultés de manipulation et de présentation des résultats. Donnons un exemple :


Nous sommes tous heureux de voir la machine réaliser la simplication suivante :

2x7 ¡ 187x6 + 1473x5 ¡ 292x4 + 553x3 + 765x2 ¡ 299x + 85


= 2x ¡ 17
x6 ¡ 85x5 + 14x4 ¡ 27x3 + 47x2 + 17x ¡ 5

Cependant, on a aussi :

x1000 ¡ 1
= x999 + x998 + ::: + 1
x¡1

et nous voudrions bien ne pas voir acher le résultat de droite qui, cette fois-ci ne nous paraît pas
du tout être une simplication.

Ce phénomène est général et a des répercussions pas seulement sur la lisibilité des résultats mais
aussi sur l'ecacité des calculs car tout le monde comprend bien qu'il est préférable de faire des
calculs sur la fraction de gauche du second exemple que de manipuler un polynôme qui possède
1000 monômes.

Ces problèmes sont délicats : en quoi une expression est elle plus simple qu'une autre aux yeux
d'un utilisateur ? En quoi est-elle plus facilement manipulable ? Ces questions tiennent autant
des mathématiques que de l'intelligence articielle, et nous concentrant sur l'aspect algébrique des
choses, nous ne l'aborderons pas, bien qu'elles soit importantes dans la conception des systèmes
formels.

2 Complexité des calculs


Pour un concepteur d'algorithme, une tâche capitale est de pouvoir a priori évaluer le temps que
prendra son algorithme pour fournir le résultat escompté sur une machine donnée. Il faut prendre
conscience du fait que de nombreux formules n'ont qu'une valeur théorique et ne débouchent sur
aucun calcul pratique car leur exécution demanderait beaucoup trop de temps. Prenons l'exemple
banal d'un calcul de déterminant d'une matrice 25  25 de nombres entiers, A = (ai;j )16i;j 625, qui
est donc une matrice de taille très modeste. La dénition même du déterminant nous indique une
méthode pour faire le calcul :
X
det (A) = sgn()a1;(1):::a25;(25)
 2S25

où S25 désigne le groupe des permutations de l'ensemble f1; :::; 25g. Un rapide calcul nous fait
sentir à quel point la programmation d'une telle formule serait absurde pour calculer det(A) : il y'a
dans cette somme 25!  1; 55:1025 produits. Imaginons, cas le plus simple, que les coecients ai; j
sont des entiers à un seul chire, de sorte que les produits de 25 facteurs intervenant dans notre
formule soit le plus simple possible; et supposons enn que notre ordinateur soit à même de calculer
10 milliards de ces produits par seconde. Combien faudra-t-il de temps pour obtenir det(A) ? Un
rapide calcul nous le dit : environ 49 000 000 années ! ...

3
Il est donc indispensable d'évaluer le temps que nécessitera un algorithme pour fournir le résultat
avant même de le programmer. Sinon on risque fort de perdre de nombreuses heures de travail en
programmation inutile, ou à attendre un résultat qui ne viendra jamais de notre vivant!...

De plus, on aimerait pouvoir comparer autant que faire se peut, les algorithmes entre eux, et ceci de
manière aussi intrinsèque que possible, c'est à dire sans que notre mesure dépende de l'ordinateur
ou du langage de programmation que l'on utilisera. Ceci nous amène à dire quelques mots de la
notion de complexité.

2.1 Complexité d'un algorithme

Le nombre d'opérations à exécuter dans l'algorithme considéré nous donne un indicateur précis
pour évaluer la durée d'exécution, ou pour comparer cet algorithme à un autre.

Restant dans le cadre d'algorithmes algébriques, et supposant que notre algorithme réalise un
certain calcul dans un ensemble E, on se propose de dénombrer les opérations à réaliser dans cet
ensemble jusqu'à obtention du résultat. On peut se demander si des opérations plus ou moins
cachées dans le programme, telles que des opérations d'écriture ou de lecture des résultats inter-
médiaires en mémoire est bien négligeable par rapport au temps mis pour réaliser les calculs.
L'expérience montre qu'il en est bien ainsi dans la grande majorité des cas et nous supposerons que
les temps de transfert des données en mémoire sont négligeables par rapport au temps de calculs
eectifs.

Bien que l'idée soit simple, deux dicultés se présentent qu'il nous faut contourner tour à tour.
D'abord quel type d'opérations devons-nous compter ? Ensuite, lorsque ce décompte s'avère di-
cile, comment peut-on en donner au moins une idée même très approximative ?

En ce qui concerne la première question, il faut d'abord remarquer que le nombre d'opération
arithmétiques pour résoudre un problème (on appelle ainsi les opérations dans l'ensemble des
données E, généralement Z; Q, ou C ou un ensemble de polynômes) ne rend pas vraiment compte
du temps de calcul : par exemple, calculer un produit de deux entiers sera quasi-instantané si ces
entiers ont dix chires chacun, alors que s'il possèdent un million de chires, la même multiplication
nécessitera tout un programme pour être eectuée, programme qui sera exécuté en un temps plus
ou moins long selon la machine, mais en tout cas non négligeable. On voit donc que le décompte
des opérations arithmétiques ne donnera pas une image dèle de la situation. On fera cependant
régulièrement le calcul du nombre d'opérations arithmétiques nécessaire à un algorithme, car
dans certains cas, réduire ce nombre provoque une réduction du temps global de calculs, mais pas
toujours.

Pour pouvoir résoudre cette question de type d'opérations à considérer, il nous faut nous rappeler,
au moins sommairement, quelques éléments de l'architecture et du fonctionnement d'un ordi-
nateur. Dans les machines actuellement construites un micro-processeur, ou plusieurs, exécute
tout le travail de l'ordinateur. Le micro-processeur, en lui même ne peut eectuer que des tâches
élémentaires : additionner, multiplier, diviser et comparer des entiers susceptibles de tenir dans
les registres dont il dispose. Ces régistres contenaient, dans les premières générations de machines,
des nombres pouvant être codés sur un octet en base 2. Aujourd'hui, on utilise des machines plus
puissantes possédant des régistres de 32, 64 voir 128 bits. C'est à dire, qu'en une seule opération,
deux entiers naturels ne dépassant pas 2128 pourront par exemple être additionnés ou multipliés.

Que faire si, dans le cadre d'une arithmétique exacte, on désire manipuler des nombres plus gros ?
Nous étudierons en détail le problème au chapitre suivant, mais disons pour le moment que la seule
ressource dont nous disposons est de créer un programme qui ramènera le problème à la manipu-
lation d'entités susamment simples pour être codées dans les registres du micro-processeur.

Ainsi, ce qui donnera une idée du coût d'un algorithme est le nombre d'opérations élémentaires
que doit réaliser le micro-processeur pour exécuter l'algorithme. C'est ce que l'on appelle la com-
plexité binaire de l'algorithme.

4
Cependant, comme nous le faisions remarquer, les micro-processeurs ont des capacités de calcul
variables; et il est clair qu'un micro-processeur ayant une vitesse double d'un autre réalisera un
même programme deux fois plus vite, et que des registres ayant une capacité double permettront
de coder une donnée algébrique en deux fois moins de place, et là encore, d'exécuter le même
algorithme proportionnellement plus vite. Bien sûr, tout ceci est très approximatif car de nombreux
facteurs technologiques entre en ligne de compte. Mais l'expérience prouve que les temps d'exécu-
tions d'algorithmes entre machines sont proportionnels, le coecient de proportionnalité étant lié
aux caractéristiques techniques des dites machines. Ainsi la complexité binaire d'un algorithme ne
peut être appréciée qu'à un coecient de proportionnalité près, lié aux capacités technologiques
de la machine; on utilise alors la notation mathématique de Landau en O : le O cache ces facteurs
technologiques que l'on ne connait pas a priori lorsqu'on étudie un algorithme de manière générale.
La complexité devient par ce biais un paramètre lié de manière intrinsèque à l'algorithme.

L'autre problème soulevé dans le calcul de la complexité d'un algorithme est celui du décompte
des opérations en lui-même; même pour un algorithme simple et facile à programmer, il n'est pas
toujours aisé de pouvoir évaluer le nombre d'opérations requises. En eet, il est parfois extrême-
ment dicile de prévoir, dans le cas de branchement conditionnels, quelle branche sera utilisé,
de même que dans certains cas on ne peut prévoir la taille des résultats intermédiaires ou naux.
Ainsi dans la plupart des cas, le calcul exact du nombre d'opérations est impossible. On se ramène,
faute de mieux, à deux types de calculs qui donnent une idées de la complexité mais de manière
bien imparfaite.

Généralement, on peut assez facilement faire un calcul de complexité au pire. Il s'agit de majorer
le nombre d'opérations nécessités par l'algorithme. On suppose pour cela que l'on se trouve dans
le plus mauvais cas possible, que les plus longues branches du programme sont toujours exécutées,
que les tailles des données croissent au plus vite. C'est cette complexité au pire que nous évaluerons
le plus fréquemment. Bien qu'elle donne une idée souvent exagérée du coût d'un algorithme, elle
permet au moins d'armer, que même dans le pire des cas, dans la plus défavorable, un algorithme
donné sera exécuté dans un temps acceptable. C'est après tout, l'un de nos soucis essentiels.

Il se trouve cependant que dans bien des cas la situation moyenne est beaucoup plus favorable
que la situation au pire qui ne se produit que très rarement. Une complexité statistique est alors
calculée soit par des méthodes probabilistes soit par une expérimentation directe de l'algorithme
sur des échantillons-tests. Cette complexité donne de précieux renseignements qui complètent la
complexité au pire : il existe en eet des algorithmes fréquemment utilisés car d'un point de vu
statistique, ils sont rapides dans presque tous les cas bien que leur complexité au pire devrait nous
décourager d'en faire usage. C'est par exemple le cas de l'algorithme de factorisation de polynômes
à coecients dans Z de Berlekamp.

Tout au long de ce cours, lorsque nous parlerons de la complexité d'un algorithme ou de son  coût ,
il s'agira de sa complexité binaire au pire à moins que nous ne précisions le contraire.

Il est bon de plus de relativiser la notion de complexité : bien que la complexité d'un algorithme
avec la notation de Landau soit un outil d'évaluation indispensable, elle ne résout cependant pas
tous les problèmes, notamment lorsqu'il s'agit de comparer des algorithmes entre eux. Il peut très
bien se faire qu'un algorithme A ait une meilleure complexité qu'un algorithme B mais qu'il soit
plus lent dans la pratique. Pourquoi ceci ? La notation O a uniquement valeur asymptotique; elle
cache une constante qui peut être énorme dans le cas de A, et très petit dans le cas de B, de sorte
que ce ne sera que pour de très grandes valeurs des paramètres que l'algorithme A sera plus rapide.

On distingue nettement les algorithmes de complexité logarithmique, polynômiale et


exponentielle. Les premiers sont considérés comme très peu coûteux et leur utilisation ne pose
aucun problème; on pense généralement des seconds qu'il rendront le résultat escompté en un temps
raisonnable. Par contre, un algorithme de complexité exponentielle donne lieu à un programme
considéré comme pratiquement inutilisable hormis sur de tous petits exemples; et aucun progrès
technique n'en viendra à bout.

5
En reprenant l'exemple du calcul du determinant d'une matrice carrée n  n à l'aide de la formule
du déterminant, on pourra montrer au chapitre suivant que sa complexité binaire au pire est de
O(n!(n )2) si  désigne le nombre maximum de chires que possèdent les coecients de la matrice,
une complexité exponentielle en n (pensez à la formule de Stirling). Comme nous l'avons observé sur
le cas 25  25, le temps de calcul est exorbitant et l'on voit bien que des calculs faits 100 fois plus
vite ne changeront pas grand chose à l'aaire. A titre comparatif, l'algorithme de Gauss réalise ce
même travail en O(n3  2) opérations binaires, une complexité polynômiale tout à fait satisfaisante.

3 Conception d'un algorithme


Nous tenons à faire maintenant quelques remarques générales sur la conception d'algorithme. Notre
but ici est de mettre l'étudiant en garde face à quelques problèmes récurrents qui se posent à nous
lorsque l'on veut transcrire une méthode mathématique en un programme aussi ecacement que
possible. Nous ne ferons pas un cours exhaustif et technique sur le sujet mais tenons à énoncer
quelques grands principes qui seront utiles tout au long des exercices. C'est ainsi que nous aborde-
rons la notion de récursivité, la technique du  diviser pour gagner , toutes deux essentielles dans
de nombreux cas.
Nous ferons ces remarques tout en traitant d'un exemple simple, le calcul de la puissance d'un
entier, xn. On peut penser que ce problème d'exponentiation est immédiat et ne se prête guère
à discussion; il n'en est rien, comme nous allons le voir. D'ailleurs, ce problème ne se pose pas
simplement dans Z mais dans tout ensemble non vide muni d'une multiplication, et, de fait, tous
les algorithmes que nous allons écrire pour calculer xn dans Z s'étendent immédiatement à un cadre
beaucoup plus général et peuvent permettre par exemple de calculer la puissance d'un polynôme
ou bien d'une matrice. Ce faisant nous allons décrire une méthode d'exponentiation qui sera utile
pour de nombreuses applications.

3.1 Procédure récursive, procédure itérative.


Une procédure est dite récursive, si lors de son exécution, elle fait appel à elle même. On dira de
même que l'algorithme ainsi implanté est récursif. De très nombreuses situations mathématiques
se prêtent bien à l'écriture d'une procédure récursive. C'est le cas par exemple du calcul de tout
objet déni par récurrence.
A l'inverse, on dira qu'une procédure est itérative, si elle n'est pas récursive. Les répétitions sont
alors gérées par des boucles dénies à l'aide de for ou de while. Il n'est pas toujours facile de
choisir le type de procédure que l'on va écrire pour traiter d'un problème donné. Dans certains
cas, alors que l'on écrit rapidement une procédure récursive, la réalisation d'une version itérative
peut s'avérer très dicile, même si l'on sait qu'elle existe.
Passons à notre exemple : la dénition de xn se fait par récurrence en posant :

x0 = 1 et pour tout n > 1; xn = x:xn¡1:

La traduction en une procédure est quasi-instantanée à condition de ne pas oublier de placer un test
d'arrêt qui permet d'indiquer à la machine lorsqu'il faut stopper le processus an que le programme
ne tourne pas indéniment...
//Calcul de x^n : version récursive
int expr (int x, int n){
if (n == 0){
return (1);
}

6
else {
return (x*expr(x,n-1));
}
}

Bien qu'immédiatement construite, cette procédure nous révélera tou aussi immédiatement ses
limites. Suivant la conguration du système de calcul formel, il sura d'essayer de calculer 2500 ou
un peu plus pour récolter un message indiquant qu'il y'a trop de niveaux de récursion et la mémoire
risque d'être saturée. En fait les systèmes de calcul formel n'attendent pas que la mémoire soit
saturée pour arrêter le processus; il possède une variable interne qui indique le nombre d'appels
récursifs tolérés. Dés que ce nombre est atteint, un message est aché et l'exécution interrompue.
Les procédures récursives sont gourmandes en mémoire, autant que faire ce peut, il est préférable
d'écrire une version itérative de la même procédure lorsque cela s'avère accessible.
Pour l'exemple du calcul de xn, écrire une procédure itérative est ici élémentaire et résout le
problème d'encombrement mémoire soulevé par la récursivité.
// Calcul de x^n : version itérative
int expi( int x, int n) {
int i, resultat = 1;
for (i = 1; i <= n; i++){
resultat = x*resultat;
}
return (resultat);
}

3.2 Diviser pour gagner


Du point de vu du nombre d'opérations, les procédures développées ci-dessus s'étudient facilement :
le calcul de xn comme nous venons de le faire, que ce soit récursivement ou itérativement, nécessite
n ¡ 1 multiplications.
Cependant, on peut se demander si on peut calculer xn en moins de n ¡ 1 multiplications ? La
réponse est positive !
Donnons l'idée : On peut remarquer que si n > 1 on a :
8 ¡ n
< x2 2 si n est pair;
x =
n  n ¡1 2
: x: x 2 si n est impair:

On appelle exponentiation dichotomique le procédé de calcul de xn à l'aide des formules que


l'on vient d'énoncer. Remarquons que la suite des puissances de x que nous avons à évaluer possède
des exposants strictement décroissants et positifs, de sorte que le processus proposés se termine
par l'appel de x0 = 1.
Ainsi par dénition même, le processus dichotomique se traduit presque immédiatement en une
procédure récursive.
//Calcul de x^n : version dichotomique récursive

7
int expdr(int x, int n){
if ( n == 0 ){
return (1);
}
else if (n % 2 == 0){
return (expdr(x,n/2) * expdr(x,n/2));
}
else {
return ( x * expdr(x,(n-1)/2) * expdr(x,(n-1)/2));
}
}
Calculons x27 en utilisant cette méthode. On a :

x27 = x:(x13)2 = x:(x:(x6)2)2 = x:(x:(x3)4)2 = x:(x:(x:x2)4)2

Assurément cette écriture n'est pas très belle; mais on voit immédiatement que 7 multiplications
seulement, au lieu de 26, ont été utilisées. On peut d'une manière générale facilement borner le
nombre de multiplications utilisées.

Proposition 2. La procédure expdr(x,n) nécessite au plus 2([log2(n)] + 1) multiplications.

Démonstration. Notons Na(n) le nombre d'appels récursifs de expdr que va eectuer la com-
mande expdr(x,n). On va montrer, par récurrence, que :

Na(n) = [log2(n)] + 1:

On a d'abord Na(1) = 1 et Na(2) = 2. Supposons la formule exacte pour toute valeur jusqu'au
n+1
rang n. Si n est impair, alors expdr(x,n+1) invoque expdr(x, 2 ) : expdr(x,n+1) appelle donc
n+1
expdr une fois, auquel s'ajoute le nombre d'appels réalisés par expdr(x, 2 ) lui même, de sorte
que l'on a dans ce cas :
   
n+1
Na(n + 1) = 1 + log2 + 1 = 1 + [log2(n + 1)]:
2

Ce qui est bien la formule désirée. Si n est pair alors expdr(x,n+1) va invoquer cette fois expdr(x,
n
2
) de sorte que l'on a dans ce cas :
h  i 
n
Na(n + 1) = 1 + log2 + 1 = 1 + [log2(n)]:
2

On n'obtient pas ainsi directement la formule escomptée mais on remarque que si n est pair, on a
[log2(n)] = [log2(n + 1)]. En eet si ceci était faux, en notant A = [log2(n + 1)], on aurait, puisque A
est entier, log2(n) < A 6 log2(n + 1), puis n < 2A 6 n + 1 et par suite 2A = n + 1; ce qui est absurde
puisque n est pair.
Pour conclure, il reste à remarquer que la procédure, à chaque appel, fait au plus deux multi-
plications (dans le cas où l'exposant passé en paramètre est impair); d'où au pire 2Na(n) multiplica-
tions 

8
Remarque 3. L'algorithme d'exponentiation dichotomique est extrêmement rapide : par cette
méthode x1000000000 se calcule en seulement 41 multiplications et on pourra apprécier le gain !
Ce n'est pas loin d'être la meilleure borne que l'on puisse espérer d'un algorithme qui calcule xn en
ne faisant que des multiplications. En eet un tel algorithme, en k multiplications ne peut dépasser
k
x2 en élevant systématiquement au carré le résultat précédent. Mais alors, si un algorithme calcule
x en k multiplications cela implique n 6 2k. Ainsi un algorithme n'utilisant que des multiplications
n

ne peut réaliser son calcul en moins de log2(n) opérations.


On s'apercevra cependant que l'exponentiation dichotomique n'est pas optimale. En reprenant
l'exemple x27, on peut écrire :

x27 = ((x3)3)3

or chaque cube nécessite 2 multiplications; ainsi x27 peut se calculer en 6 multiplications.

Diviser pour gagner : Pour construire des algorithmes, il n'y a pas beaucoup de techniques pour
nous guider. L'une des rares méthode vient d'être appliquer dans l'exponentiation dichotomique :
la technique du  diviser pour gagner . De quoi s'agit-il exactement ?
Pour réaliser une certaine manipulation sur des données D, on subdivise la masse des données en
des paquets de taille à peu près identique, et, supposant que l'on a déjà exécuté la manipulation
désirée sur chaque paquet, on regarde comment, partant de l'acquis, on peut réaliser le travail sur
les données D elles-mêmes. Bien sûr une telle démarche se prête très bien à une écriture récursive
puisqu'on répètera la  division pour gagner  sur les données morcelées jusqu'à n'avoir à traiter que
des données élémentaire sur lesquelles le travail est immédiat. De nombreuses situations s'adaptent
bien à ce type de programmation. Par exemple dans l'exponentiation dichotomique, on suppose
déjà calculer x[n/2] pour en déduire xn.

Exemple. (Tri-fusion) Imaginons que l'on ait à ranger par ordre croissant une liste de d nombres.
On la divise en deux listes de d/2 nombres (on suppose ici d pair), on range ces deux demi-listes
puis on les interlasse. Ceci se fera rapidement puisque, pour trouver le nouvel élément à placer
dans la liste nale, il sut de comparer les premiers éléments non-encore utilisés des deux sous-
liste de sorte que cet interlacement se fait en d/2 comparaisons, cette méthode de tri est l'une des
plus rapide dont nous disposions.

Ces algorithmes construit sur la technique du  diviser pour gagner  possèdent une structure
relativement identique et l'étude de leur complexité suit la même méthode. Si T (d) désigne le
temps de calcul pour réaliser une tâche donnée sur les d données et si l'on découpe les données en
k parts identiques (on suppose d divisible par k), alors on peut écrire :

T (d) = kT (d/k) + C(d; k)

où C(d; k) désigne le temps nécéssaire pour nir d'accomplir la tâche désirée sur toute les données
lorsqu'elle a déjà été eectuée sur chacun des k paquets. Une telle relation s'appelle une recurrence
de partition. Pour obtenir un équivalent, on regarde généralement ce qui se passe pour les d de la
forme k n, puis faisant l'hypothèse raisonnable que la fonction T est croissante, on en déduit un
encadrement de T (d) dans le cas général.

4 Quelques exercices.
Exercice 1. (Tri-fusion) Programmer le tri d'une liste de nombres en utilisant le tri-fusion. On note C(n) le
nombre de comparaisons à eectuer pour trier par cette méthode n nombres. Donner une formule permettant
de calculer C(2k) en fonction de C(2k ¡1). Supposant que la fonction C(n) est croissante, en déduire que C(n) =
O(n log(n)).

9
Exercice 2. (Algorithme de Hörner)

a) On veut évaluer un polynôme à coecients entiers P (x) = adxd + :::: + a0 en un nombre . Montrer que
la suite dénie par P0 = ad, Pi = Pi¡1 + ad¡i à son d-ième terme qui vaut P ( ). Considérer le gain de
multiplications ainsi réalisé par rapport à une évaluation naïve de P ( ). Cet algorithme est connu sous
le nom de l'algorithme de Hörner.

b) En supposant que n s'écrit en base 2 ("k ; :::; "1; "0)2, écrire une procédure qui calcule xn en utilisant
l'algorithme de Hörner pour évaluer n. Comparer-le à l'algorithme d'exponentiation dichotomique pro-
posé dans le cours.

c) Quel est alors le nombre exact de multiplications necessaires pour évaluer xn par dichotomie. A titre
d'exemple, montrer que x1000000 se fait en 25 multiplications exactement en dichotomie.

Exercice 3. (Calcul d'une suite récurrente linéaire)


Soit la suite récurrente dénie par :

u0 = 1; u1 = 0
un+1 = 2un + un¡1:

L'objectif de cet exercice est d'envisager quatre manières diérentes de calculer un et de les comparer.

1. Écrire en langage C, une fonction récursive qui calcule un. Soit An le nombre d'appels récursifs réalisés
par la fonction qui calcule un. Montrer que An vérie une relation de récurrence simple et qu'enn ce
nombre d'appels est exponentiel en fonction de n.

2. Écrire en langage C, une fonction itérative qui calcule un. Combien d'opérations arithmétiques sont alors
nécéssaires pour calculer un ?
 
un ¡1
3. On note vn = un
. Montrer alors qu'il existe une matrice M telle que vn+1 = Mvn. Écrire une nouvelle
fonction itérative pour calculer un.

4. Démontrer que :
p ! p !
1 2 ¡ p n 1 2 ¡ p n
un = ¡ 1+ 2 + + 1¡ 2 (1)
2 4 2 4

5. Bien que la formule (1) soit une manière intéressante d'un point de vu mathématique de présenter un, a
t'elle un intérêt calculatoire ? Justier votre réponse.

6. Utiliser l'exponantiation dichotomique pour calculer M n et en déduire un algorithme permettant


d'obtenir un en O(log(n)) opérations arithmétiques.

10

Vous aimerez peut-être aussi