La dichotomie n'est pas le seul exemple d'algorithme qui découpe les données pour les traiter séparément. La méthode générale est appelée "diviser pour régner" (ou parfois en anglais "
divide and conquer", en référence aux souverains qui pensaient que séparer leurs sujets en groupes bien distincts permettait de les gouverner plus facilement - cela évitait, par exemple, qu'ils se regroupent pour demander des augmentations de salaire, ou ce genre de choses gênantes). Il est important de noter que ces algorithmes ne font pas qu'éliminer, comme la dichotomie, une partie des données, mais qu'ils peuvent profiter de la subdivison pour effectuer moins de calculs.
On peut mettre en application cette idée de façon très astucieuse pour calculer les puissances d'un nombre. Pour ceux qui ne seraient pas au courant, x à la puissance n, noté x
n, vaut x multiplié par lui-même n fois : x * x * ... * x , avec n termes 'x', et x
0 vaut 1 (c'est une convention naturelle et pratique). Cette opération est très utile, par exemple si on se demande "quelle est la quantité de nombres différents qui ont au plus 3 chiffres ?" : la réponse est 10
3 ; de 0 à 999, il y a 10
3 = 10 * 10 * 10 = 1000 nombres.
Avec cette définition, comment calculer x
8 ? Il est évident qu'une bonne réponse est : (x * x * x * x * x * x * x * x). Si l'on demande à l'ordinateur de calculer ça, il va effectuer 7 multiplications (vous pouvez compter). Plus généralement, on peut calculer x
n en effectuant n-1 multiplications : c'est un algorithme linéaire, en O(n).
Exercice : implémentez cette méthode simple de calcul de la puissance d'un nombre.
Cependant, on peut faire mieux, en remarquant que x
8 = x
4 * x
4 : si l'on calcule x
4 (avec la méthode simple, x
4 = x * x * x * x, cela fait 3 multiplications), il suffit de le multiplier ensuite par lui-même pour obtenir x
8, en faisant seulement une opération supplémentaire, soit 4 au total. On peut faire encore mieux en utilisant le fait que x
4 = x
2 * x
2 : on peut calculer x
4 en deux multiplications, donc x
8 en trois multiplications : c'est beaucoup mieux que les 7 multiplications initiales.
C'est un algorithme de "diviser pour régner", parce qu'en découpant le problème (calcul de x
8) en deux sous-problèmes (deux calculs de x
4), on s'est rendu compte qu'on avait énormément simplifié la question : il suffit de s'intéresser à un des sous-problèmes et le deuxième tombe avec, puisqu'on peut réutiliser le résultat du premier calcul (en faisant une multiplication supplémentaire). Vous avez peut-être déjà reconnu la configuration qui commence à être familière : pour passer de x
4 à x
8, donc en doublant la difficulté, il suffit de rajouter une opération (une multiplication) ; il y a du logarithme dans l'air !
Cependant, il reste un problème à résoudre : avec cette bonne idée, on peut calculer facilement x
2, x
4, x
8, x
16, x
32, etc., mais comment faire pour les nombres qui sont moins "gentils" comme 7, 13 ou 51 ?
Plus généralement, on sait comment réduire efficacement le problème du calcul de x
n quand n est pair : on a x
n = x
n/2 * x
n/2. Quand n est impair, la division par 2 donne un reste, cette méthode n'est donc pas utilisable directement. Cependant, il suffit de calculer x
n-1 (ce qui est facile puisque si n est impair, alors n-1 est pair), et de multiplier ensuite par x pour obtenir x
n :
- x0 = 1 ;
- si n est pair, xn = xn/2 * xn/2 ;
- si n est impair, xn = x(n-1) * x.
Cet algorithme nous permet de calculer récursivement x
n en très peu de multiplications. On l'appelle "exponentiation rapide", parce qu'exponentiation veut dire "mise à la puissance", et qu'il est rapide.
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14 | <?php
function exponentiation_rapide($x, $n) {
if ($n == 0) {
return 1;
} else if ($n % 2 == 0) {
$a = exponentiation_rapide($x, $n / 2);
return $a * $a;
} else {
return $x * exponentiation_rapide($x, $n - 1);
}
}
echo exponentiation_rapide(2, 10);
?>
|
Une petite remarque sur le code : le cas du milieu,
if ($n % 2 ==
0), est le cas important puisque c'est lui qui met en place la "bonne idée" qui permet d'aller plus vite. Ce qui fait qu'il est efficace, c'est qu'on enregistre le résultat de
exponentiation_rapide($x, $n /
2) pour le calculer une seule fois. Si l'on avait écrit à la place
return exponentiation_rapide($x, $n / 2) * exponentiation_rapide($x,
$n / 2); , notre algorithme aurait fait deux fois le même calcul, tuant complètement l'intérêt de cette méthode : on serait revenu à l'algorithme linéaire expliqué plus tôt, mais codé de façon plus compliquée.
Voyons maintenant sa complexité : on a montré que quand on double n, il suffit de faire une opération de plus : pour les puissances de 2, il suffit de log(n) opérations pour calculer x
n. Mais cela ne fonctionne pas pour les nombres impairs : si on double n et qu'on lui ajoute 1, on obtient un nombre impair et il faut faire
deux opérations de plus. Mais ce n'est pas grave, car "deux opérations" et "une opération", c'est presque la même chose : cela ne diffère qu'à un coefficient multiplicatif près. Au pire, on fait deux fois plus d'opérations (2*log(n)), mais cela conserve la même complexité : l'algorithme d'exponentiation rapide a une complexité logarithmique (autrement dit, en O(log N)).
Il faut savoir que c'est un algorithme très important parce qu'il apparaît dans des situations très diverses. En effet, on l'utilise ici pour faire des puissances de nombres réels, mais il est beaucoup plus général que ça : on peut l'utiliser sur tout un tas d'objets mathématiques divers (il suffit de pouvoir définir l'opération de "mise à la puissance" sur ces objets), et il permet alors de faire les choses les plus étranges.
Par exemple, si vous connaissez la suite de Fibonacci (ou si vous aimez vous renseigner sur Wikipédia), il existe de nombreux algorithmes permettant de calculer le n-ième terme de cette suite : un de ces algorithmes utilise l'exponentiation rapide, et il est extrêmement efficace (c'est un des plus rapides qui existe, beaucoup plus rapide que le calcul des termes de 1 à n).