Résumé
Les leçons précédentes de l’unité ont appris à écrire des boucles et à raisonner sur elles avec un invariant et un variant ; cette leçon applique ce raisonnement à quatre algorithmes numériques classiques, plus anciens que les ordinateurs et toujours en usage. Le test de primalité par divisions successives par essai, qui s’arrête à pour une raison que l’on démontre, et dont la condition d’arrêt déborde si on l’écrit sans soin. L’algorithme d’Euclide pour le plus grand commun diviseur, avec son invariant, sa terminaison, son piège avec les nombres négatifs et sa comparaison avec std::gcd. Le changement de base par divisions successives et le schéma de Horner, qui reconstruit un nombre à partir de ses chiffres et évalue un polynôme selon le même schéma. Et la dichotomie (bissection), qui approche une racine en coupant un intervalle en deux, garantie par le théorème des valeurs intermédiaires, avec un nombre d’itérations que l’on calcule d’avance. Chaque algorithme est présenté comme un objet mathématique : avec une preuve qu’il fait ce qu’il dit, une preuve qu’il termine et un premier décompte de ses étapes.
Objectifs d’apprentissage
- Implémenter le test de primalité par divisions jusqu’à et justifier pourquoi il suffit d’aller jusqu’à .
- Implémenter l’algorithme d’Euclide, énoncer son invariant (le plus grand commun diviseur ne change pas) et le comparer avec
std::gcd. - Convertir un nombre dans une autre base par divisions successives et évaluer une représentation positionnelle, ou un polynôme, avec le schéma de Horner.
- Approcher une racine par dichotomie jusqu’à une tolérance, et calculer combien d’itérations elle exige.
Diviseurs et nombres premiers
Divisions successives par essai
Un entier est premier si ses seuls diviseurs positifs sont 1 et ; s’il en a un autre, il est composé ; 0 et 1 ne sont ni l’un ni l’autre. La définition suggère aussitôt un algorithme : essayer chaque candidat entre 2 et et déclarer le nombre composé dès que l’un d’eux le divise. C’est la boucle du plus petit diviseur de la leçon Boucles, et elle est correcte. Or, pour un nombre premier de dix chiffres, elle effectue presque dix milliards de divisions, et presque toutes sont inutiles, pour la raison suivante.
Si est composé, il a un diviseur avec .
Démonstration
Si est composé, il s’écrit comme produit de deux facteurs, dont aucun n’est égal à 1 ni à . On nomme le plus petit des deux.
En multipliant par le nombre strictement positif , on obtient , d’où : le plus petit facteur de toute factorisation est à gauche de .
Les diviseurs se présentent donc par paires symétriques autour de : avec , les paires sont , , et . Si aucun candidat jusqu’à ne divise , aucun au-delà ne le divise non plus, car son partenaire serait apparu avant.
Il s’ensuit que le nombre de divisions passe d’environ à environ : pour proche de deux milliards, de deux milliards à quarante-six mille. Le programme utilise le schéma du drapeau de la leçon Concevoir des boucles correctes : isPrime commence vrai et la boucle l’éteint si elle trouve un diviseur.
#include <iostream>#include <print>int main(){ int n{}; std::cin >> n; bool isPrime{n >= 2}; for (int d{2}; isPrime && d <= n / d; ++d) { if (n % d == 0) { isPrime = false; } } if (isPrime) { std::println("{} est premier", n); } else { std::println("{} n’est pas premier", n); }}21474836472147483647 est premierTrois décisions méritent un commentaire. Le drapeau est initialisé avec n >= 2, de sorte que 0, 1 et les nombres négatifs sont déclarés non premiers sans entrer dans la boucle : oublier ce cas est l’erreur typique, car avec la boucle ne fait aucune itération et le programme déclarerait 1 premier. La condition du for a deux parties : le drapeau, qui arrête la boucle au premier diviseur, et la borne. Et la borne s’écrit d <= n / d, ce qui, pour des entiers strictement positifs, équivaut à (la division entière tronque, mais ne dépasse pas le quotient tronqué exactement quand il ne dépasse pas , car est entier). Le nombre de l’entrée, , est le plus grand int et il est premier ; le programme le décide avec restes, ceux des candidats de 2 à .
L’invariant de la boucle est : aucun entier de 2 à ne divise , tant que le drapeau reste vrai. Il est vrai à l’entrée, quand l’intervalle de 2 à 1 est vide ; il se conserve, car si ne divise pas , l’intervalle s’agrandit d’un nombre qui ne le divise pas ; et à la sortie avec le drapeau vrai, l’invariant et la condition fausse, , disent ensemble qu’aucun candidat jusqu’à ne divise , ce qu’exige la proposition. Le variant est le nombre de candidats qui restent à essayer, c’est-à-dire d’entiers entre et : il décroît d’une unité par itération et ne descend pas au-dessous de zéro.
La condition qui déborde
La manière naturelle d’écrire la borne est d * d <= n, et c’est une erreur. Avec l’entrée précédente, la boucle atteint , et alors le produit dépasse le maximum de int.
#include <iostream>#include <print>int main(){ int n{}; std::cin >> n; bool isPrime{n >= 2}; for (int d{2}; isPrime && d * d <= n; ++d) { if (n % d == 0) { isPrime = false; } } std::println("Premier ? {}", isPrime);}2147483647main.cpp:9:33: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'Le programme s’est terminé avec le code 1.
Le détecteur de comportement indéfini arrête le programme au produit. Sans lui, le dépassement de capacité signé est un comportement indéfini, et la norme ne dit pas ce qui se passe. Ce que l’on observe avec GCC sous Linux, avec et sans optimisation, est instructif : le produit fait le tour et devient négatif ou plus petit que , et comme aucun int ne dépasse le maximum, la condition d * d <= n ne cesse plus d’être vraie ; la boucle continue d’essayer des candidats pendant environ deux milliards d’itérations, jusqu’à atteindre , qui divise , et le programme déclare composé un nombre premier. Une erreur de dépassement ne se manifeste pas comme un dépassement, mais comme une réponse fausse. La forme d <= n / d ne peut pas déborder, car elle divise au lieu de multiplier ; la leçon Bits et bibliothèque mathématique offrait une autre issue, calculer la borne une fois avec std::sqrt, mais alors l’exactitude de la borne dépend de l’arrondi de la virgule flottante, et la division entière n’a pas ce problème.
La leçon « Nombres premiers et théorème fondamental de l’arithmétique », du cours Algèbre et géométrie I, démontre que tout entier strictement supérieur à 1 est un produit de nombres premiers d’une seule manière ; les divisions successives par essai sont la manière la plus directe de trouver ce produit, en divisant par chaque diviseur trouvé et en continuant avec le quotient.
Chargement du contenu…