Présentation du problème.
On se donne un monoïde \((G,*)\), un élément \(x\in G\) et \(k\in\N\). On cherche à calculer la puissance \(x^k\), c'est-à-dire la quantité \begin{equation} \underbrace{x*x*\cdots*x}_{k\ \text{termes} \ x}. \end{equation} avec par convention \(x^0=e\) où \(e\) est l'élément neutre de la loi \(*\). Si \((G,*)\) est de surcroît un groupe, on généralise aux exposants \(k\in\Z\) avec \(x^k=(x^{-1})^{-k}\) si \(k<0\) et où \(x^{-1}\) désigne le symétrique de \(x\) pour la loi \(*\) considérée, i.e. \(x*x^{-1}=x^{-1}*x=e\). Ce problème ne se limite donc pas aux calculs de puissances de nombres réels.
Pour ne citer que deux exemples, l'exponentiation est utilisée intensivement dans de nombreux protocoles cryptographiques, comme rsa sur le groupe \((\Z/n\Z,\times)\) et en algèbre linéaire où les puissances d'une matrice interviennent dans des contextes variés. Nous supposerons donc dans toute la suite que \(x\) est un élément d'un monoïde \((G,*)\) noté multiplicativement, que \(k\) est un entier naturel et que \(e\) est l'élément neutre pour la loi \(*\).
L'algorithme naïf qui consiste à multiplier la valeur \(x\) par elle-même \(k - 1\) fois dans le monoïde a un coût proportionnel à l'exposant \(k\) si l'on suppose que la multiplication a un coût fixe. Cette dernière hypothèse est extrêmement restrictive selon le monoïde considéré. En effet, il est clair que si l'on multiplie des entiers relatifs, la taille de la représentation du résultat ne reste pas bornée, nous y reviendrons au chapitre Calcul multiprécision.
L'algorithme.
L'idée sur laquelle repose l'algorithme de l'exponentiation binaire est très naturelle. En calculant le carré \(x^2\) de \(x\), puis le carré de \(x^2\), on obtient \(x^4\) en deux multiplications au lieu des trois nécessaires avec l'algorithme naïf. On peut continuer le processus et obtenir \(x^8\) en trois multiplications au lieu de sept, etc. À première vue, ce procédé ne paraît intéressant que pour les exposants \(k\) qui sont des puissances de 2, mais les règles de calcul sur les exponentielles permettent de dépasser cette apparente limitation. En effet, on rappelle que si \(a\) et \(b\) désignent deux entiers, alors
\[x^{a+b}=x^a * x^b.\]
Ainsi, comme tout entier \(k\) peut s'écrire de manière unique comme une somme de puissances de \(2\) (c'est ce qu'exprime son écriture en base 2), il existe donc une séquence \((k_i)\) de \(n\) bits tels que
\[k:=\sum_{i=0}^{n-1} k_i\, 2^i,\]
on a donc
\begin{equation}
\label{eq:result}
x^k=x^{\small\displaystyle\left(\sum_{i=0}^{n-1}k_i\, 2^i\right)}=\prod_{i=0}^{n-1} x^{k_i\, 2^i}.
\end{equation}
La table suivante illustre le mécanisme pour k = 87 et l'écriture de k en base 2 est donnée du chiffre le plus significatif à gauche au chiffre le moins significatif à droite.
\(k=87\) : | 1 | 0 | 1 | 0 | 1 | 1 | 1 |
Exposant \(i\) : | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Puissance de 2 : | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
(S)quare : | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) |
(M)ultiply : | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) | \(\checkmark\) |
On peut reconstituer \(x^k\) avec deux opérateurs uniquement, l'élévation au carré (noté S pour Square) et la multiplication par x (noté M pour Multiply). En effet, en initialisant une variable \(R\) à l'élément neutre \(e\) pour la multiplication, il suffit de lire successivement les bits de l'écriture binaire de l'entier \(k\) (notés k[i] dans l'algorithme) dans l'ordre des puissances décroissantes de \(2\), et pour chaque bit lu :
SquareMultiply(x,k):valeur; données x: valeur // dans le monoide considéré k: entier de n bits // exposant variables R: valeur i: entier debut R ← e i ← 0 TQ (i < n) FAIRE R ← R * R SI (k[n - (i + 1)] > 0) ALORS R ← R * x FSI i ← i + 1 FTQ RENVOYER R fin
\(i\) | \(k[n-(i+1)]\) | (S) | (M) | \(R\) (après opération) | |
\(e\) | |||||
0 | 1 | \(\checkmark\) | \(\checkmark\) | \(x\) | |
1 | 0 | \(\checkmark\) | \(x^2\) | ||
2 | 1 | \(\checkmark\) | \(\checkmark\) | \(x^4\times x\) | |
3 | 0 | \(\checkmark\) | \(x^8\times x^2\) | ||
4 | 1 | \(\checkmark\) | \(\checkmark\) | \(x^{16}\times x^4\times x\) | |
5 | 1 | \(\checkmark\) | \(\checkmark\) | \(x^{32}\times x^8\times x^2\times x\) | |
6 | 1 | \(\checkmark\) | \(\checkmark\) | \(x^{64}\times x^{16}\times x^4\times x^2\times x\) |
La preuve d'arrêt est immédiate, une seule instruction modifie la variable \(i\) dans la boucle, elle est incrémentée, ses valeurs constituent donc une suite strictement croissante de premier terme \(0\), et comme \(N\) est archimédien, la condition d'arrêt \(\color{yellow}i\geq n\) sera donc satisfaite après \(n+1\) itérations.
Considérons le prédicat \(P(i)\) suivant : \begin{equation} \label{eq:Rp} \text{après \(i\) itérations de la boucle}\ \ R=\prod_{j=0}^{i-1}x^{k_{n-(j+1)}2^{i-(j+1)}}. \end{equation} Avant d'entrer la boucle, \(P(0)\) est vrai puisque par convention le produit vide est égal à l'élément neutre \(e\) pour l'opérateur \(*\) considéré. Supposons que \(P(i)\) soit vrai à l'entrée de la boucle. Après l'élévation au carré à la ligne #12, on a \[ R=\prod_{j=0}^{i-1}x^{k_{n-(j+1)}2^{i-(j+1)+1}}. \] En notant que \[x^{k_{n-(i+1)}}= \begin{cases}x&\text{si}\ k_{n-(i+1)}=1\\ 1&\text{si}\ k_{n-(i+1)}=0 \end{cases} \] Le bloc conditionnel #13 à #15 évite simplement le calcul \(x^{k_{n-(i+1)}}\) et ne réalise une multiplication par \(x\) que si c'est nécessaire. Après son exécution, on a donc \[ R=x^{k_{n-(i+1)}}\prod_{j=0}^{i-1}x^{k_{n-(j+1)}2^{i-(j+1)+1}}. \] En réécrivant l'expression (noter que \(2^{i-(i+1)+1}=2^0=1\)), on obtient \[ R=\prod_{j=0}^{i}x^{k_{n-(j+1)}2^{i-(j+1)+1}} \] et \(P(i+1)\) est vrai. Comme \(i=n\) en sortant de la boucle, on a \(P(n)\), soit \begin{align*} R &=\prod_{j=0}^{n-1}x^{k_{n-(j+1)}2^{n-(j+1)}}\\ &=\prod_{i=0}^{n-1} x^{k_i\times 2^i}\quad(\text{en réindexant}\ i:=n-(j+1)) \end{align*} ce qui est bien le résultat attendu \((\ref{eq:result})\).
Complexité.
Plutôt que la complexité au sens strict, nous allons évaluer le coût \(C(n)\) de l'algorithme exprimé en nombre de produits en fonction du nombre de chiffres de l'exposant \(k\). Ce choix est légimité par le fait que l'algorithme s'applique à n'importe quel monoïde, un calcul de complexité strict imposerait d'intégrer le coût de chaque produit, or l'algorithme auxiliaire qui effectue ce produit dépend de la nature du monoïde considéré, un produit d'entiers n'est pas équivalent à un produit de matrices par exemple.
La preuve du lemme suivant est à faire en exercice (voir plus bas), ne la dépliez qu'après avoir tenté votre chance…
Notons \(n\) le nombre de bits de l'exposant \(k\), c'est-à-dire \(n:=\lfloor\log_{2}k\rfloor+1\) d'après le lemme ci-dessus. Nous savons que le nombre de multiplications utilisées pour les élévations au carré (S) est exactement \(n\) et que le nombre de multiplications pour l'opération (M) est égal au poids binaire \(\text{wt}_2(k)\) de l'entier \(k\). Comme le nombre de bits \(n\) est fixé, le bit de poids fort de \(k\) est nécessairement égal à \(1\) et donc \(1\leq\text{wt}_2(k)\leq n\). Au total, le nombre de multiplications pour traiter l'exposant \(k\) de \(n\) bits est égal à \(n+\text{wt}_2(k)\) et on a l'encadrement suivant : \begin{equation} n+1\leq C(n)\leq 2n. \end{equation} si le nombre de multiplications \(C(n)\) est exprimé en fonction du nombre de bits de \(k\). On en déduit directement la proposition
Il reste à évaluer le cas moyen. On sait que quelle que soit l'instance \(k\) de taille \(n\) que l'on considère pour l'exposant, le nombre de multiplications pour calculer les carrés (S) est toujours égal à \(n\). D'autre part le bit de poids fort de \(k\) étant nécessairement égal à 1, il y a toujours une multiplication (M) à la première étape. Ce sont les \(n-1\) bits de poids faible qui vont fixer le nombre de multiplications qu'il reste à effectuer. Il y a donc \(2^{n-1}\) instances différentes à étudier, correspondant aux différentes valeurs possibles des \(n-1\) bits de poids faible.
Le nombre d'opérations (M) étant égal au poids binaire de \(k\), on peut calculer la moyenne en partitionnant l'ensemble \(K\) des séquences binaires de longueur \(n-1\) en \(n\) classes selon les différents poids possibles :
\[K_p:=\left\{u\in\{0,1\}^{n-1}\mid \text{wt}_2(u) = p\right\},\quad p\in\ab{0}{n-1}.\]On a donc à ce stade
\[\overline{C}(n)=n+1+\frac{1}{2^{n-1}} \sum_{p=0}^{n-1}p|K_p|. \]On veut dans un premier temps calculer les cardinaux \(|K_p|\). On rappelle que si \(p\) et \(n\) désignent deux entiers naturels avec \(0\leq p\leq n\), le nombre de parties à \(p\) éléments dans un ensemble à \(n\) élements, noté \(\binom{n}{p}\) est égal à
\[\binom{n}{p}=\frac{n!}{(n-p)!p!}.\]En montre aisément que
On en déduit que \(|K_p|=\binom{n}{p}\), le coût moyen devient
\[\overline{C}(n)=n+1+\frac{1}{2^{n-1}} \color{yellow}{\sum_{p=0}^{n-1}p\binom{n-1}{p}}. \]Il nous reste à calculer la somme ci-dessus.
Travaux pratiques
Le langage utilisé dans tous les sujets qui suivent est le \(C\)./usr/bin/time --format="%U" a.out >/dev/null
Mesurez le temps d'exécution de vos deux programmes pour des valeurs croissantes de \(n\) sans dépasser la capacité d'un ullong. Tracez les graphes des temps d'exécution en fonction de \(n\) de ces deux algorithmes avec la commande Gnuplot.