Pour tous a∈Z et b∈Z∗, il existe un unique couple (q,r)∈Z2 tel que a=bq+r et 0≤r<∣b∣. L'entier q est le quotient et r le reste de la division euclidienne de a par b. Lu en français : on retranche b à a autant de fois qu'il le faut pour tomber dans l'intervalle [0,∣b∣[, et r est ce qui reste. Ce résultat est établi dans le thème Divisibilité, PGCD et Bézout dans Z (bloc Algèbre 3) : on s'en sert ici sans le redémontrer. Tous les algorithmes de ce thème, sans exception, sont bâtis sur ce couple (q,r).
∀a∈Z,∀b∈Z∗,∃!(q,r)∈Z2,a=bq+ret0≤r<∣b∣
existence et unicité du quotient et du reste
r=0⟺b∣a
un reste nul, c'est exactement la divisibilité
Propriété
Quotient et reste en Python : le reste suit le signe du diviseur
Pour tous entiers a et b avec b non nul, Python garantit deux choses : l'identité a == b * (a // b) + a % b, et le fait que a % b a le signe du diviseurb (ou vaut zéro). L'opérateur // est le quotient par défaut : il arrondit vers −∞, et non vers zéro. Conséquence pratique : quand b est strictement positif, a % b appartient à {0,…,b−1} et coïncide avec le reste de la division euclidienne, même sia est négatif ; quand b est strictement négatif, ce n'est plus le reste au sens de la définition. Dans tout ce thème on travaille sur des entiers naturels ou l'on prend des valeurs absolues, ce qui place systématiquement dans le premier cas.
a=b⌊ba⌋+(amodb)
le quotient de Python est la partie entière par défaut
b>0⟹0≤amodb≤b−1
avec un diviseur positif, le reste de Python est celui de la division euclidienne
Piège
Attendre un reste négatif pour un dividende négatif
Ligne fautive : « -7 % 3 vaut -1, comme en C ou en Java. » C'est faux : en Python, -7 % 3 vaut 2 et -7 // 3 vaut -3, ce qui est cohérent puisque 3×(−3)+2=−7. Le quotient est arrondi vers −∞, pas vers zéro, et le reste reste dans {0,1,2}. L'erreur symétrique existe aussi : 7 % -3 vaut -2 et non 1, parce que le reste prend le signe du diviseur. Version correcte : sur des entiers naturels, a % b est le reste de la division euclidienne, sans précaution. Dès qu'un signe peut apparaître, on écrit la précondition dans la spécification et l'on ramène les données en valeur absolue avant la boucle, ou l'on teste sur un exemple chiffré avant de se fier à son intuition.
Propriété
Entiers exacts, et ce que l'on compte comme opération élémentaire
En Python, le type int est exact et de taille non bornée : 2 ** 1000 est calculé sans débordement ni arrondi, alors qu'un flottant perdrait toute précision. Ce confort a un prix : une addition ou une multiplication d'entiers n'est à coût constant que tant que les entiers tiennent sur quelques mots machine. Dans ce thème on adopte la convention usuelle des concours : on compte les opérations arithmétiques (addition, multiplication, division euclidienne, comparaison) comme élémentaires, et l'on annonce séparément le cas où les entiers grossissent, ce qui arrive avec l'exponentiation rapide non réduite. Rappel hors ligne, en une phrase : f=O(g) signifie qu'il existe une constante C>0 et un rang à partir duquel ∣f∣≤Cg (thème Négligeabilité, domination et équivalents, bloc Analyse 2). Le modèle de comptage lui-même est celui du thème Complexité des algorithmes.
f=O(g)⟺∃C>0,∃n0∈N,∀n≥n0,∣f(n)∣≤Cg(n)
la domination, rappelée pour mémoire
Vocabulaire
Les mots de l'arithmétique programmée
Diviser : b divise a, noté b∣a, s'il existe k∈Z tel que a=bk. Quotient et reste : les deux entiers de la division euclidienne, a // b et a % b. PGCD : le plus grand des diviseurs communs. Premiers entre eux : deux entiers dont le PGCD vaut 1 ; on dit aussi étrangers. Coefficients de Bézout : un couple (u,v) d'entiers tel que au+bv=pgcd(a,b). Nombre premier : un entier supérieur ou égal à 2 dont les seuls diviseurs positifs sont 1 et lui-même. Composé : un entier supérieur ou égal à 2 qui n'est pas premier. Valuation p-adique : l'exposant de p dans la décomposition en facteurs premiers. Cribler : marquer d'avance les multiples pour ne plus avoir à les tester. Degré d'un polynôme non nul : l'indice du dernier coefficient non nul.
Définition
PGCD de deux entiers
Soient a,b∈Z non tous deux nuls. L'ensemble des diviseurs communs de a et b est non vide (il contient 1) et majoré (par max(∣a∣,∣b∣)), donc il admet un plus grand élément : c'est le PGCD de a et b, noté pgcd(a,b). On complète par la convention pgcd(0,0)=0, qui rend les formules valables sans cas particulier. Deux propriétés servent en permanence dans les preuves de ce thème : pgcd(a,b)=pgcd(∣a∣,∣b∣), et pgcd(m,0)=∣m∣ pour tout entier m. Toutes deux se lisent sur la définition : un entier et son opposé ont les mêmes diviseurs, d'où la première ; et tout entier divise 0, donc les diviseurs communs de m et de 0 sont exactement les diviseurs de m, dont le plus grand vaut ∣m∣ dès que m=0, le cas m=0 étant réglé par la convention ci-dessus. C'est cette seconde égalité qui fournit la valeur renvoyée par l'algorithme d'Euclide au moment où il s'arrête.
pgcd(a,b)=max{d∈N∗:d∣aetd∣b}
le plus grand des diviseurs communs, pour deux entiers non tous deux nuls
pgcd(m,0)=∣m∣etpgcd(a,b)=pgcd(∣a∣,∣b∣)
le cas d'arrêt de la boucle, et l'indifférence au signe
Propriété
Invariant du PGCD
Propriété : pour tous a∈Z et b∈Z∗, pgcd(a,b)=pgcd(b,amodb). Démonstration : posons r=amodb et q=adivb, de sorte que a=bq+r. Si d divise b et r, alors d divise bq+r=a, donc d est un diviseur commun de a et b. Réciproquement, si d divise a et b, alors d divise a−bq=r, donc d est un diviseur commun de b et r. Les deux couples ont donc exactement les mêmes diviseurs communs, donc le même plus grand. L'hypothèse b=0 est indispensable : sans elle, amodb n'est pas défini, et c'est précisément pourquoi la boucle d'Euclide s'arrête quand le second argument s'annule. C'est cette égalité, et elle seule, qui fait de l'algorithme d'Euclide un algorithme correct : elle dit que le couple change à chaque tour, mais que la quantité cherchée, elle, ne bouge pas.
∀a∈Z,∀b∈Z∗,pgcd(a,b)=pgcd(b,amodb)
le pas de l'algorithme d'Euclide, sous forme d'égalité
{d:d∣aetd∣b}={d:d∣betd∣(amodb)}
ce qui est vraiment conservé : l'ensemble entier des diviseurs communs
Méthode
Programmer l'algorithme d'Euclide et prouver sa correction par l'invariant du PGCD
Quand l'utiliser : dès qu'un énoncé demande un PGCD, une fraction irréductible, un test « ces deux entiers sont-ils premiers entre eux », ou sert de brique à Bézout. Le signal dans l'énoncé : deux entiers et le mot « diviseur commun », « irréductible » ou « étrangers ». 1) Écrire la spécification avant le code : ce que la fonction reçoit, ce qu'elle renvoie, sa précondition (voir le thème Python : structures de contrôle et fonctions pour le format de la docstring). Ici : « Renvoie le PGCD des entiers a et b », précondition « a et b sont des entiers ». 2) Ramener les arguments en valeur absolue, puisque pgcd(a,b)=pgcd(∣a∣,∣b∣) et que le reste de Python suit le signe du diviseur. 3) Écrire la boucle avec une affectation simultanée, jamais deux affectations successives : a, b = b, a % b. 4) Pour la terminaison, exhiber le variant : la valeur de b, entier naturel, qui décroît strictement à chaque tour puisque a % b est inférieur à b. 5) Pour la correction, exhiber l'invariant : « le PGCD du couple courant est celui du couple initial ». La méthode générale, variant puis invariant puis sortie de boucle, est celle du thème Algorithmes élémentaires et preuve de programmes ; on l'applique ici sans la redire. L'initialisation est immédiate, l'hérédité est exactement la propriété pgcd(a,b)=pgcd(b,amodb). 6) Conclure en sortie de boucle : b vaut 0, l'invariant donne pgcd(a,0)=pgcd(a0,b0), et pgcd(a,0)=a. C'est la ligne que les copies oublient : sans elle, on a prouvé qu'une quantité se conserve, pas que le programme renvoie le bon nombre.
Ik:pgcd(ak,bk)=pgcd(a0,b0)
l'invariant, écrit comme une propriété indexée par le numéro du tour
Vk=bk∈N,Vk+1<Vk
le variant : un entier naturel qui décroît strictement
Exemple
Rédaction modèle : correction et terminaison de l'algorithme d'Euclide
Écrire une fonction pgcd(a, b) qui renvoie le PGCD de deux entiers, puis démontrer qu'elle termine et qu'elle renvoie bien pgcd(a,b). Programme.
def pgcd(a, b):
"""Renvoie le PGCD des entiers a et b (un entier naturel).
Precondition : a et b sont des entiers."""
a, b = abs(a), abs(b)
while b != 0:
a, b = b, a % b
return a
Notations. Notons a0 et b0 les valeurs de a et b juste après la ligne des valeurs absolues, et, pour k≥1, ak et bk leurs valeurs après le k-ième tour de boucle, lorsque celui-ci a lieu. Par construction a0=∣a∣ et b0=∣b∣ sont des entiers naturels. Terminaison. Posons Vk=bk. Chaque bk est un entier naturel, comme reste d'une division par un entier strictement positif ou comme valeur absolue de départ. Si le tour k+1 a lieu, c'est que bk=0, donc bk≥1 ; alors bk+1=akmodbk vérifie 0≤bk+1<bk d'après la division euclidienne, donc Vk+1<Vk. La suite (Vk) est une suite d'entiers naturels strictement décroissante tant que la boucle tourne : elle ne peut donc prendre qu'un nombre fini de valeurs, et la boucle s'arrête. Correction. Considérons, pour chaque tour effectué k, la propriété Ik : « pgcd(ak,bk)=pgcd(a0,b0) ». I0 est vraie, les deux membres étant le même nombre. Supposons Ik vraie et le tour k+1 effectué. Alors bk=0, et l'affectation simultanée donne ak+1=bk et bk+1=akmodbk. D'après l'invariant du PGCD, applicable puisque bk=0, on a pgcd(bk,akmodbk)=pgcd(ak,bk), donc pgcd(ak+1,bk+1)=pgcd(ak,bk), qui vaut pgcd(a0,b0) par hypothèse de récurrence. Donc Ik+1 est vraie, et par récurrence Ik est vraie pour tout tour effectué. Conclusion. Notons K le nombre de tours effectués, fini d'après la terminaison. En sortie de boucle, bK=0, et IK donne pgcd(aK,0)=pgcd(a0,b0). Or pgcd(m,0)=m pour tout entier naturel m, donc la valeur renvoyée aK vaut pgcd(a0,b0), c'est-à-dire pgcd(∣a∣,∣b∣), c'est-à-dire pgcd(a,b). Trace sur a=1071 et b=462 : les valeurs successives de b sont 462, puis 147, puis 21, puis 0, et la fonction renvoie 21 après trois tours. À retenir : une preuve de boucle se rédige en quatre blocs nommés, variant, initialisation, hérédité, sortie de boucle, et c'est la sortie de boucle qui transforme un invariant en valeur renvoyée.
Propriété
Coût de l'algorithme d'Euclide
Propriété : notons r0=a, r1=b et rk+2=rkmodrk+1 tant que rk+1=0, avec a et b entiers naturels et b≥1. Alors, pour tout k≥1 tel que rk+2 soit défini, rk+2<2rk. Il en résulte que le nombre de divisions euclidiennes effectuées est majoré par 2log2(b)+2, donc que l'algorithme d'Euclide fait O(log(min(∣a∣,∣b∣))) divisions. La condition k≥1 n'est pas une coquetterie : si a<b, le tout premier tour se contente d'échanger les deux arguments, et l'on a r2=a=r0. C'est à partir du rang 1 que la suite des restes décroît strictement. Théorème de Lamé (admis, hors programme du raisonnement demandé) : le pire cas est atteint par deux termes consécutifs de la suite de Fibonacci, et le nombre de divisions est alors majoré par cinq fois le nombre de chiffres décimaux du plus petit argument. Pour le couple (610,377), l'algorithme fait 13 divisions, quand la majoration 2log2(377)+2 en annonce au plus 19.
∀k≥1,rk+2<2rk
en deux divisions, le reste est au moins divisé par deux
#{divisions}≤2log2(b)+2=O(logb)
le coût d'Euclide est logarithmique en le plus petit argument
Exemple
Rédaction modèle : le nombre de divisions d'Euclide est logarithmique
Soient a et b deux entiers naturels avec b≥1. On pose r0=a, r1=b et, tant que rk+1=0, rk+2=rkmodrk+1. Montrer que pour tout k≥1 tel que rk+2 soit défini, rk+2<2rk, puis majorer le nombre de divisions effectuées par l'algorithme d'Euclide. Soit k≥1 tel que rk+2 soit défini ; en particulier rk+1=0 et rk=0. Comme k≥1, l'entier rk+1 est le reste de la division de rk−1 par rk, donc rk+1<rk. Distinguons deux cas selon la position de rk+1 par rapport à 2rk. Premier cas : rk+1≤2rk. Comme rk+2 est le reste de la division de rk par rk+1, on a rk+2<rk+1, donc rk+2<2rk. Second cas : rk+1>2rk. Comme rk+1<rk, le quotient de rk par rk+1 vaut exactement 1, donc rk+2=rk−rk+1. Or rk+1>2rk, donc rk−rk+1<rk−2rk=2rk, donc rk+2<2rk. Dans les deux cas rk+2<2rk, ce qui établit la propriété annoncée. Majoration du nombre de divisions. Notons K le premier indice tel que rK=0 : la suite des restes est r0,r1,…,rK, et le nombre de divisions effectuées est K−1. Montrons par récurrence sur j≥1 que r1+2j<2jb lorsque cet indice est défini. Pour j=1, la propriété établie au rang k=1 donne r3<2r1=2b. Si r1+2j<2jb, alors la propriété au rang k=1+2j, qui est bien supérieur ou égal à 1, donne r3+2j<2r1+2j<2j+1b. Si K≤3, il y a au plus deux divisions, et la majoration annoncée est vraie puisque b≥1. Supposons K≥4 et posons j=⌊2K−2⌋, de sorte que j≥1 et 1+2j≤K−1. L'indice 1+2j étant inférieur ou égal à K−1, le reste r1+2j est non nul, donc r1+2j≥1. De 1≤r1+2j<2jb on tire 2j<b, donc j<log2(b). Or j≥2K−3, donc K−3≤2j<2log2(b), donc K−1<2log2(b)+2 : l'algorithme d'Euclide effectue O(logb) divisions. Vérification numérique : pour a=1071 et b=462, la majoration annonce au plus 19 divisions, et l'algorithme en fait 3 ; pour le pire cas (610,377), il en fait 13. À retenir : pour majorer le coût d'une boucle arithmétique, on cherche une quantité qui est divisée par un facteur fixe en un nombre borné de tours, et l'on passe au logarithme.
Exemple
Rédaction modèle : l'algorithme d'Euclide récursif et sa correction par récurrence forte
Écrire une version récursive de l'algorithme d'Euclide et démontrer sa correction. Programme.
def pgcd_recursif(a, b):
"""Renvoie le PGCD des entiers naturels a et b.
Precondition : a >= 0 et b >= 0."""
if b == 0:
return a
return pgcd_recursif(b, a % b)
Terminaison. Le second argument est un entier naturel qui décroît strictement à chaque appel : si b=0, l'appel suivant reçoit amodb, qui vérifie 0≤amodb<b. Une suite strictement décroissante d'entiers naturels étant finie, la suite des appels s'arrête, nécessairement sur le cas de base b=0. Correction. Montrons par récurrence forte sur b∈N la propriété P(b) : « pour tout a∈N, l'appel pgcd_recursif(a, b) termine et renvoie pgcd(a,b) ». Initialisation. Pour b=0, le test b == 0 est vrai et la fonction renvoie a ; or pgcd(a,0)=a pour tout entier naturel a. Donc P(0) est vraie. Hérédité. Soit b≥1 ; supposons P(c) vraie pour tout entier naturel c<b. Soit a∈N. Comme b=0, la fonction renvoie la valeur de pgcd_recursif(b, a % b). Or amodb est un entier naturel strictement inférieur à b, donc l'hypothèse de récurrence forte appliquée à c=amodb garantit que cet appel renvoie pgcd(b,amodb). D'après l'invariant du PGCD, pgcd(b,amodb)=pgcd(a,b), donc l'appel initial renvoie pgcd(a,b), et P(b) est vraie. Par récurrence forte, P(b) est vraie pour tout b∈N. Profondeur de la pile. Le nombre d'appels emboîtés est le nombre de divisions, majoré par 2log2(b)+2, et plus finement, par le théorème de Lamé, par cinq fois le nombre de chiffres décimaux de b. Pour b de moins de 150 chiffres, la profondeur reste donc sous 750, en deçà de la limite de récursion par défaut de Python, qui vaut 1000 et compte tous les appels déjà empilés (voir le thème Récursivité et diviser pour régner). Au-delà, la version récursive échoue là où la version itérative continue de tourner : pour deux termes consécutifs de la suite de Fibonacci dont le plus petit a trois cents chiffres, qui est le pire cas, l'algorithme demande jusqu'à 1436 divisions et l'appel lève RecursionError. C'est un argument de plus en faveur de la boucle dès que les entiers sont de taille cryptographique. À retenir : une fonction récursive se prouve par récurrence sur la quantité qui décroît, ici le second argument, et l'hypothèse de récurrence doit être forte parce que l'appel ne porte pas sur b−1 mais sur un entier quelconque plus petit que b.
Piège
Remplacer l'affectation simultanée par deux affectations successives
Ligne fautive :
def pgcd_faux(a, b):
while b != 0:
a = b
b = a % b
return a
La première ligne écrase a, si bien que la seconde calcule b % b, qui vaut toujours 0 : la boucle s'arrête après un unique tour et renvoie l'ancien b. Contre-exemple chiffré : cette version renvoie 8 pour pgcd(12, 8) au lieu de 4, et 462 pour pgcd(1071, 462) au lieu de 21. Le programme ne plante pas, il ne boucle pas non plus : il renvoie tranquillement un faux résultat, ce qui est le pire des trois. Version correcte : a, b = b, a % b. En Python, le membre de droite est intégralement évalué avant toute affectation : le a utilisé dans a % b est bien l'ancien. Réflexe de relecture : dans une boucle qui fait tourner deux variables, l'échange s'écrit sur une seule ligne, ou bien il faut une variable temporaire.
Piège
Oublier de ramener les arguments en valeur absolue
Ligne fautive : une fonction pgcd sans la ligne a, b = abs(a), abs(b), au motif que « le reste est toujours positif ». Il ne l'est pas : le reste de Python suit le signe du diviseur. Contre-exemple chiffré : avec a = 12 et b = -18, on a 12 % -18 qui vaut -6, puis -18 % -6 qui vaut 0, et la fonction renvoie -6 alors que le PGCD vaut 6. Avec a = -12 et b = 18, en revanche, elle renvoie 6 : le bogue ne se déclenche que si le diviseur devient négatif, donc il passe à travers un jeu de tests trop pauvre. Version correcte : a, b = abs(a), abs(b) en première ligne, justifiée par pgcd(a,b)=pgcd(∣a∣,∣b∣), et une précondition explicite dans la docstring. Le jeu de tests doit contenir les quatre combinaisons de signes, et pas seulement deux entiers naturels (voir le thème Python : modules, fichiers et tests).
Propriété
Théorème de Bézout
Théorème : pour tous a,b∈Z, il existe (u,v)∈Z2 tel que au+bv=pgcd(a,b). Un tel couple (u,v) s'appelle un couple de coefficients de Bézout de a et b. Corollaire : a et b sont premiers entre eux si et seulement s'il existe (u,v)∈Z2 tel que au+bv=1. Le sens direct est le théorème ; la réciproque tient à ce que tout diviseur commun de a et b divise alors 1. Rappel hors ligne, en une phrase : ce théorème et ses conséquences (lemme de Gauss, caractérisation des entiers premiers entre eux) sont démontrés dans le thème Divisibilité, PGCD et Bézout dans Z, bloc Algèbre 3. Ce qui nous occupe ici est différent et strictement informatique : l'existence du couple ne dit pas comment le calculer, et c'est l'algorithme d'Euclide étendu qui le construit, en O(log(min(∣a∣,∣b∣))) divisions.
∀(a,b)∈Z2,∃(u,v)∈Z2,au+bv=pgcd(a,b)
le PGCD s'écrit comme combinaison entière des deux nombres
pgcd(a,b)=1⟺∃(u,v)∈Z2,au+bv=1
caractérisation des entiers premiers entre eux
Méthode
Programmer l'algorithme d'Euclide étendu par des triplets
Quand l'utiliser : quand l'énoncé demande non seulement le PGCD, mais aussi une écritureau+bv=d : coefficients de Bézout, inverse modulaire, résolution d'une équation diophantienne, restes chinois. Le signal dans l'énoncé : les mots « déterminer un couple (u,v) tel que », « inverse modulo », « résoudre dans Z2 ». 1) Écrire la spécification : « Renvoie un triplet (d, u, v) tel que d soit le PGCD de a et b et que a * u + b * v == d », précondition « a et b sont des entiers naturels non tous deux nuls ». 2) Faire porter la boucle non plus sur deux nombres, mais sur deux triplets(r,u,v) et (r′,u′,v′), chacun accompagné de son écriture en fonction de a et b. 3) Initialiser avec les deux écritures évidentes : a=a×1+b×0 et b=a×0+b×1, soit (r, u, v) = (a, 1, 0) et (r_suivant, u_suivant, v_suivant) = (b, 0, 1). 4) À chaque tour, calculer une seule fois le quotient q = r // r_suivant, puis appliquer la même combinaison aux trois composantes : le nouveau triplet est l'ancien second, et le second devient l'ancien premier moins q fois l'ancien second. 5) Énoncer l'invariant à deux volets : « r=au+bv et r′=au′+bv′ », et « pgcd(r,r′)=pgcd(a,b) ». Le premier volet justifie l'égalité de Bézout, le second justifie que la valeur renvoyée est bien le PGCD. 6) Conclure en sortie de boucle : r′=0, donc r=pgcd(r,0)=pgcd(a,b), et le premier volet donne au+bv=r. 7) Vérifier le résultat par le calcul a * u + b * v == d sur deux ou trois exemples : ce test coûte une ligne et attrape toutes les erreurs de recopie de la combinaison.
(r,u,v)←(r′,u′,v′),(r′,u′,v′)←(r−qr′,u−qu′,v−qv′)
un seul quotient, la même combinaison sur les trois composantes
r=au+bvetr′=au′+bv′
l'invariant qui porte l'égalité de Bézout tout au long de la boucle
Exemple
Rédaction modèle : Euclide étendu, son invariant et sa correction
Écrire une fonction qui, pour deux entiers naturels a et b non tous deux nuls, renvoie un triplet (d, u, v) avec d le PGCD de a et b et a * u + b * v == d. Démontrer sa correction. Programme.
def euclide_etendu(a, b):
"""Renvoie un triplet (d, u, v) tel que d = pgcd(a, b) et a * u + b * v = d.
Precondition : a >= 0, b >= 0, a et b non tous deux nuls."""
r, u, v = a, 1, 0
r_suivant, u_suivant, v_suivant = b, 0, 1
while r_suivant != 0:
q = r // r_suivant
r, r_suivant = r_suivant, r - q * r_suivant
u, u_suivant = u_suivant, u - q * u_suivant
v, v_suivant = v_suivant, v - q * v_suivant
return r, u, v
Notations. Notons (rk,uk,vk) et (rk′,uk′,vk′) les valeurs des six variables après k tours de boucle, avec k=0 pour les valeurs initiales. Terminaison. La suite (rk′) est une suite d'entiers naturels, et si le tour k+1 a lieu, rk+1′=rkmodrk′ vérifie 0≤rk+1′<rk′. C'est donc un variant, et la boucle s'arrête ; le nombre de tours est celui de l'algorithme d'Euclide ordinaire, majoré par 2log2(b)+2. Correction. Considérons la propriété Ik : « rk=auk+bvk, rk′=auk′+bvk′ et pgcd(rk,rk′)=pgcd(a,b) ». Initialisation. On a r0=a=a×1+b×0 et r0′=b=a×0+b×1, et pgcd(r0,r0′)=pgcd(a,b) : I0 est vraie. Hérédité. Supposons Ik vraie et le tour k+1 effectué, donc rk′=0. Posons q=rkdivrk′. Les affectations donnent rk+1=rk′, uk+1=uk′, vk+1=vk′, ainsi que rk+1′=rk−qrk′, uk+1′=uk−quk′ et vk+1′=vk−qvk′. La première écriture est acquise par hypothèse de récurrence, puisque rk+1=rk′=auk′+bvk′=auk+1+bvk+1. Pour la seconde, on calcule auk+1′+bvk+1′=a(uk−quk′)+b(vk−qvk′)=(auk+bvk)−q(auk′+bvk′)=rk−qrk′=rk+1′. Enfin rk+1′=rkmodrk′, donc l'invariant du PGCD donne pgcd(rk+1,rk+1′)=pgcd(rk′,rkmodrk′)=pgcd(rk,rk′)=pgcd(a,b). Donc Ik+1 est vraie, et par récurrence Ik l'est pour tout tour effectué. Conclusion. Notons K le nombre de tours. En sortie, rK′=0, donc pgcd(rK,0)=pgcd(a,b), c'est-à-dire rK=pgcd(a,b) puisque rK est un entier naturel. Le premier volet de IK donne alors auK+bvK=rK=pgcd(a,b) : le triplet renvoyé convient. Trace sur a=240 et b=46. Les triplets successifs sont (240,1,0) et (46,0,1), puis, pour les quotients 5, 4, 1, 1 et 2 : (46,0,1) et (10,1,−5) ; (10,1,−5) et (6,−4,21) ; (6,−4,21) et (4,5,−26) ; (4,5,−26) et (2,−9,47) ; enfin (2,−9,47) et (0,23,−120). La fonction renvoie (2,−9,47), et l'on vérifie 240×(−9)+46×47=−2160+2162=2. À retenir : on ne calcule pas les coefficients de Bézout « à la remontée » sur une copie, on les transporte dans la boucle avec le reste, et l'invariant s'écrit comme deux égalités que le quotient q combine à l'identique.
Exemple
Rédaction modèle : la version récursive de l'algorithme d'Euclide étendu
Écrire une version récursive de l'algorithme d'Euclide étendu et démontrer sa correction. Programme.
def euclide_etendu_recursif(a, b):
"""Renvoie un triplet (d, u, v) tel que d = pgcd(a, b) et a * u + b * v = d.
Precondition : a >= 0, b >= 0, a et b non tous deux nuls."""
if b == 0:
return a, 1, 0
d, u, v = euclide_etendu_recursif(b, a % b)
return d, v, u - (a // b) * v
D'où vient la dernière ligne. L'appel récursif fournit d, u et v tels que bu+(amodb)v=d. Or amodb=a−⌊ba⌋b, donc d=bu+(a−⌊ba⌋b)v=av+b(u−⌊ba⌋v). Les coefficients cherchés pour le couple (a,b) sont donc v et u−⌊ba⌋v, dans cet ordre : ils s'obtiennent par échange puis correction, ce qui explique le return d, v, u - (a // b) * v. Correction. Montrons par récurrence forte sur b∈N la propriété P(b) : « pour tout a∈N avec (a,b)=(0,0), l'appel renvoie un triplet (d,u,v) tel que d=pgcd(a,b) et au+bv=d ». Initialisation. Pour b=0, la fonction renvoie (a,1,0) ; or pgcd(a,0)=a et a×1+0×0=a. Donc P(0) est vraie. Hérédité. Soit b≥1, et supposons P(c) vraie pour tout entier naturel c<b. Soit a∈N. Comme amodb est un entier naturel strictement inférieur à b, l'hypothèse de récurrence forte s'applique au couple (b,amodb) : l'appel récursif renvoie (d,u,v) avec d=pgcd(b,amodb) et bu+(amodb)v=d. D'une part, l'invariant du PGCD donne d=pgcd(b,amodb)=pgcd(a,b). D'autre part, le calcul ci-dessus donne av+b(u−⌊ba⌋v)=d, qui est exactement l'égalité de Bézout pour le triplet renvoyé. Donc P(b) est vraie, et par récurrence forte elle l'est pour tout b∈N. Vérification numérique : euclide_etendu_recursif(240, 46) renvoie (2,−9,47), le même triplet que la version itérative. À retenir : dans une version récursive, l'assemblage du résultat se démontre en réécrivant l'égalité rendue par l'appel plus petit en fonction des données de l'appel courant ; c'est ce calcul, et non l'intuition, qui dicte l'ordre des composantes du return.
Piège
Croire que le couple de coefficients de Bézout est unique
Ligne fautive : « L'algorithme d'Euclide étendu renvoie le couple (u,v) tel que au+bv=d, donc mon résultat doit être celui du corrigé. » Il n'y a pas unicité. Si (u0,v0) convient, alors pour tout k∈Z le couple (u0+dbk,v0−dak) convient aussi, car les deux termes ajoutés se compensent. Contre-exemple chiffré avec a=240 et b=46, dont le PGCD vaut 2 : l'algorithme renvoie (u,v)=(−9,47), et l'on vérifie 240×(−9)+46×47=2 ; mais (14,−73) convient tout autant, puisque 240×14+46×(−73)=3360−3358=2. Un jeu de tests qui compare le triplet renvoyé à une valeur écrite en dur rejettera donc une implémentation correcte. Version correcte : tester la propriété, pas la valeur. On écrit d, u, v = euclide_etendu(a, b) puis on vérifie d == math.gcd(a, b) and a * u + b * v == d. Sur une copie, on écrit « un couple de coefficients de Bézout » et non « le couple », et l'on précise, si l'énoncé le demande, la contrainte supplémentaire qui rend le couple unique (par exemple 0≤u<db).
Méthode
Résoudre en nombres entiers l'équation au + bv = c
Quand l'utiliser : l'énoncé demande de résoudre dans Z2 une équation du premier degré à deux inconnues entières, ou de décider si un montant est atteignable avec deux pas donnés. Le signal : « résoudre dans Z2 », « déterminer tous les couples d'entiers tels que ». 1) Calculer d=pgcd(a,b) et un couple (u0,v0) tel que au0+bv0=d, avec euclide_etendu(a, b). 2) Tester la condition d'existence : l'équation a des solutions si et seulement sid divise c. Écrire la phrase sur la copie : « Comme d∣c, l'équation admet des solutions » ou « Comme d∤c, elle n'en admet aucune », en justifiant par le fait que d divise au+bv pour tous entiers u et v. 3) Construire une solution particulière en multipliant par dc : (dcu0,dcv0). 4) Écrire l'ensemble des solutions : ce sont les couples (dcu0+dbk,dcv0−dak) pour k∈Z. Ne pas oublier de diviser a et b par d : sans cela on n'obtient qu'une partie des solutions. 5) En Python, renvoyer la solution particulière et le pas, pas une liste infinie : une fonction qui renvoie (c // d * u0, c // d * v0, b // d, a // d) décrit toute la famille. Le couple (u0, v0) rendu par euclide_etendu résout au+bv=d et non au+bv=c : c'est sa mise à l'échelle par dc, celle de l'étape 3, qui est la solution particulière. 6) Vérifier sur deux valeurs de k que l'égalité tient : le contrôle coûte une ligne et attrape l'oubli de la division par d.
pgcd(a,b)∣c
condition nécessaire et suffisante d'existence, à écrire avant tout calcul
(dcu0+dbk,dcv0−dak),k∈Z
toutes les solutions, à partir du couple de Bézout (u0,v0) de l'étape 1 remis à l'échelle
Résoudre dans Z2 l'équation 240u+46v=6. Calcul du PGCD et d'un couple de Bézout. L'appel euclide_etendu(240, 46) renvoie (2,−9,47) : on a pgcd(240,46)=2 et 240×(−9)+46×47=2. Existence. Tout entier de la forme 240u+46v est divisible par 2, puisque 2 divise 240 et 46. Comme 2 divise 6, l'équation admet des solutions. Solution particulière. En multipliant l'égalité de Bézout par 26=3, on obtient 240×(−27)+46×141=6. Le couple (u0,v0)=(−27,141) est donc solution. Analyse. Soit (u,v)∈Z2 une solution. En retranchant membre à membre les deux égalités, 240(u+27)+46(v−141)=0, donc 120(u+27)=−23(v−141) après division par 2. Ainsi 23 divise 120(u+27). Or pgcd(23,120)=1, donc, d'après le lemme de Gauss, 23 divise u+27 : il existe k∈Z tel que u+27=23k, c'est-à-dire u=−27+23k. En reportant, 120×23k=−23(v−141), donc v−141=−120k, c'est-à-dire v=141−120k. Synthèse. Réciproquement, pour tout k∈Z, 240(−27+23k)+46(141−120k)=6+(240×23−46×120)k=6+(5520−5520)k=6, donc tous ces couples sont solutions. Conclusion. L'ensemble des solutions est {(−27+23k,141−120k):k∈Z}. Par exemple k=1 donne (−4,21), et l'on vérifie 240×(−4)+46×21=−960+966=6. À retenir : on écrit toujours les trois temps, existence par divisibilité du second membre, solution particulière par mise à l'échelle du couple de Bézout, puis analyse et synthèse pour la famille complète, dont le pas est db et −da, jamais b et −a.
Méthode
Inverser un entier modulo n
Quand l'utiliser : l'énoncé demande « l'inverse de a modulo n », « résoudre ax≡1(modn) », ou fait apparaître une division dans Z/nZ, notamment en cryptographie. Le signal : une congruence à inverser, ou un exposant de déchiffrement à calculer. 1) Poser la question d'existence : a est inversible modulo nsi et seulement sipgcd(a,n)=1. Le sens direct vient de ce que ax−1 multiple de n force tout diviseur commun de a et n à diviser 1 ; la réciproque est le théorème de Bézout. 2) Appliquer euclide_etendu(a % n, n) pour obtenir (d,u,v) avec (amodn)u+nv=d. C'est bien amodn, et non a, qui figure dans cette égalité : l'algorithme ne voit jamais a lui-même, et l'écrire avec a donne une égalité fausse dès que a≥n. 3) Contrôler que d vaut 1, par une assertion qui fait échouer bruyamment le programme plutôt que de renvoyer un faux inverse. 4) Réduire : la relation (amodn)u+nv=1 donne (amodn)u≡1(modn) ; comme a et amodn sont congrus modulo n, on en tire au≡1(modn), donc u % n est l'inverse cherché dans {0,…,n−1}. La réduction finale n'est pas cosmétique : u est souvent négatif. 5) Vérifier par a * x % n == 1, en une ligne, avant de se servir du résultat.
def inverse_modulaire(a, n):
"""Renvoie l'unique entier x de {0, ..., n - 1} tel que a * x % n == 1.
Precondition : n >= 2 et pgcd(a % n, n) == 1."""
d, u, _ = euclide_etendu(a % n, n)
assert d == 1, "a n'est pas inversible modulo n"
return u % n
ainversible modulon⟺pgcd(a,n)=1
condition d'existence de l'inverse
au+nv=1⟹au≡1(modn)
le coefficient de Bézout devient l'inverse une fois réduit
Exemple
Rédaction modèle : l'inverse de 17 modulo 43
Déterminer l'inverse de 17 modulo 43, en justifiant son existence et son unicité dans {0,…,42}. Existence. L'algorithme d'Euclide donne successivement 43=2×17+9, puis 17=1×9+8, puis 9=1×8+1, puis 8=8×1+0. Le dernier reste non nul vaut 1, donc pgcd(17,43)=1 et 17 est inversible modulo 43. Coefficients de Bézout, par remontée. De la troisième égalité, 1=9−8. En remplaçant 8 par 17−9 tiré de la deuxième égalité, 1=9−(17−9)=2×9−17. En remplaçant 9 par 43−2×17 tiré de la première égalité, 1=2(43−2×17)−17=2×43−5×17. On a donc 17×(−5)+43×2=1. Réduction. De 17×(−5)≡1(mod43) et −5≡38(mod43), on tire 17×38≡1(mod43). Vérification : 17×38=646=43×15+1, donc 646≡1(mod43). Unicité. Si x et y appartiennent à {0,…,42} et vérifient 17x≡17y≡1(mod43), alors 17(x−y)≡0(mod43), donc 43 divise 17(x−y) ; comme pgcd(17,43)=1, le lemme de Gauss donne que 43 divise x−y. Or ∣x−y∣≤42<43, donc x=y. Conclusion. L'inverse de 17 modulo 43 est 38. Contrôle par le programme : euclide_etendu(17, 43) renvoie (1,−5,2), et inverse_modulaire(17, 43) renvoie 38. À retenir : la remontée d'Euclide sur une copie et la boucle sur les triplets en machine font le même calcul ; on rédige la remontée à la main et l'on vérifie que le programme donne le même couple, au multiple de dn près.
Propriété
Relation de l'exponentiation rapide
Propriété : soit x un élément d'un ensemble muni d'une multiplication associative et d'un neutre 1 (un entier, un rationnel, un flottant, une matrice carrée, une classe modulo m). Alors x0=1 et, pour tout n∈N∗, xn=(x⌊n/2⌋)2 si n est pair, et xn=x⋅(x⌊n/2⌋)2 si n est impair. Démonstration : si n=2m, alors ⌊n/2⌋=m et xn=x2m=(xm)2 ; si n=2m+1, alors ⌊n/2⌋=m et xn=x2m+1=x⋅(xm)2. L'associativité suffit, la commutativité n'est pas requise : c'est pourquoi le même algorithme calcule une puissance de matrice (produit matriciel, thème Calcul matriciel du bloc Algèbre 2) sans une ligne de changement. Tout l'intérêt tient dans le mot une fois : on calcule x⌊n/2⌋ une seule fois et on l'élève au carré, au lieu de multiplier n−1 fois. L'exposant est divisé par deux à chaque étape, donc il ne reste qu'environ log2n étapes.
xn={(xn/2)2x⋅(x(n−1)/2)2si n pairsi n impair
la relation de récurrence sur laquelle repose tout l'algorithme
n=i=0∑pbi2i⟹xn=i=0∏p(x2i)bi
lecture binaire : on multiplie les carrés itérés dont le bit vaut un
Méthode
Programmer l'exponentiation rapide itérative et prouver sa correction par son invariant
Quand l'utiliser : dès qu'un exposant peut être grand, ou qu'il est une variable et non un petit nombre écrit dans l'énoncé. Le signal : « calculer xn pour n de l'ordre de 109 », « calculer An », « calculer xnmodm », ou une suite récurrente linéaire à écrire matriciellement. 1) Écrire la spécification : « Renvoie x ** n », précondition « n est un entier naturel ». La précondition n'est pas décorative : avec un exposant négatif, la boucle n'est jamais exécutée et le programme renvoie silencieusement 1. 2) Déclarer trois variables : resultat, initialisé au neutre 1 ; base, initialisée à x ; exposant, initialisé à n. 3) Écrire la boucle while exposant > 0 avec, dans cet ordre : si l'exposant est impair, multiplier resultat par base ; puis élever base au carré ; puis diviser exposant par deux en division entière. 4) Énoncer l'invariant, qui est le cœur de la preuve : resultat * base ** exposant vaut toujours x ** n. Sur une copie, on l'écrit en mathématiques : Rk⋅BkEk=xn. 5) Démontrer l'hérédité par un seul calcul, en séparant le cas pair et le cas impair, ou d'un coup en écrivant Ek=2⌊Ek/2⌋+(Ekmod2). 6) Donner le variant : exposant, entier naturel divisé par deux à chaque tour, donc strictement décroissant tant qu'il est non nul. 7) Conclure en sortie de boucle : exposant vaut 0, donc l'invariant se lit R⋅B0=R=xn.
Rk⋅BkEk=xn
l'invariant de l'exponentiation rapide, vrai avant et après chaque tour
Ek=2⌊2Ek⌋+(Ekmod2)
l'égalité qui fait marcher l'hérédité d'un seul calcul
Exemple
Rédaction modèle : exponentiation rapide, invariant, variant et trace
Écrire une fonction itérative qui calcule xn pour n entier naturel, puis démontrer sa terminaison et sa correction. Programme.
def puissance_rapide(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
resultat = 1
base = x
exposant = n
while exposant > 0:
if exposant % 2 == 1:
resultat = resultat * base
base = base * base
exposant = exposant // 2
return resultat
Notations. Notons Rk, Bk et Ek les valeurs de resultat, base et exposant après k tours de boucle, avec R0=1, B0=x et E0=n. Terminaison. La suite (Ek) est à valeurs dans N. Si le tour k+1 a lieu, c'est que Ek≥1, et alors Ek+1=⌊Ek/2⌋≤Ek/2<Ek. La suite (Ek) est donc un variant : entière, positive, strictement décroissante tant que la boucle tourne. La boucle s'arrête. Correction. Considérons la propriété Ik : « Rk⋅BkEk=xn ». Initialisation. R0⋅B0E0=1⋅xn=xn : I0 est vraie. Hérédité. Supposons Ik vraie et le tour k+1 effectué. Posons m=⌊Ek/2⌋ et s=Ekmod2, de sorte que Ek=2m+s avec s∈{0,1}. Les affectations donnent Rk+1=Rk⋅Bks (la multiplication n'a lieu que si s=1), Bk+1=Bk2 et Ek+1=m. Alors Rk+1⋅Bk+1Ek+1=Rk⋅Bks⋅(Bk2)m=Rk⋅Bks+2m=Rk⋅BkEk, qui vaut xn par hypothèse de récurrence. Donc Ik+1 est vraie, et par récurrence Ik l'est pour tout tour effectué. Conclusion. Notons K le nombre de tours. En sortie, EK=0, donc IK s'écrit RK⋅BK0=xn, c'est-à-dire RK=xn : la valeur renvoyée est bien xn. Trace sur x=3 et n=13. Avant la boucle, (R,B,E)=(1,3,13). Après le premier tour, (3,9,6) ; après le deuxième, (3,81,3) ; après le troisième, (243,6561,1) ; après le quatrième, (1594323,43046721,0). La fonction renvoie 1594323, et 313=1594323. À retenir : l'invariant d'une exponentiation rapide n'est pas « resultat vaut une puissance de x », mais l'égalité complète R⋅BE=xn, qui contient les trois variables ; c'est elle, évaluée en E=0, qui donne le résultat.
Propriété
Coût de l'exponentiation rapide
Propriété : pour n∈N∗, la fonction puissance_rapide effectue exactement (⌊log2n⌋+1)+s(n) multiplications, où s(n) est le nombre de chiffres égaux à 1 dans l'écriture binaire de n. En particulier ce nombre est majoré par 2(⌊log2n⌋+1), donc l'algorithme effectue Θ(logn) multiplications, contre n−1 pour la boucle naïve. Justification : la boucle fait un tour par chiffre binaire de n, soit ⌊log2n⌋+1 tours, chacun comportant une élévation au carré ; la multiplication supplémentaire n'a lieu qu'aux tours où le chiffre courant vaut 1, d'où le terme s(n). On note au passage que la toute dernière élévation au carré est inutile : le résultat n'est plus lu. Vérification numérique : 13 demande 7 multiplications (quatre chiffres binaires, trois chiffres 1), 1000 en demande 16, et 106 en demande 27, quand la boucle naïve en demanderait 999999. Attention : ce décompte porte sur le nombre de multiplications, non sur leur coût unitaire. Si x est un entier et que l'on ne réduit pas modulo un entier m, les facteurs grossissent et une multiplication n'est plus à coût constant.
C(n)=(⌊log2n⌋+1)+s(n)≤2(⌊log2n⌋+1)=Θ(logn)
le nombre exact de multiplications, puis sa majoration
2(⌊log2n⌋+1)n−1n→+∞+∞
l'écart avec la boucle naïve n'est pas un facteur constant
Exemple
Rédaction modèle : la version récursive et son coût par relation de récurrence
Écrire une version récursive de l'exponentiation rapide, démontrer sa correction, et établir son coût par une relation de récurrence. Programme.
def puissance_rapide_recursive(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
if n == 0:
return 1
demi = puissance_rapide_recursive(x, n // 2)
if n % 2 == 0:
return demi * demi
return demi * demi * x
Correction. Montrons par récurrence forte sur n∈N la propriété P(n) : « l'appel puissance_rapide_recursive(x, n) termine et renvoie xn ». Initialisation. Pour n=0, la fonction renvoie 1=x0, donc P(0) est vraie. Hérédité. Soit n≥1 ; supposons P(m) vraie pour tout entier naturel m<n. Comme n≥1, on a ⌊n/2⌋<n, donc l'hypothèse de récurrence forte s'applique : la variable demi reçoit x⌊n/2⌋. Si n est pair, la fonction renvoie (x⌊n/2⌋)2=x2⌊n/2⌋=xn. Si n est impair, elle renvoie (x⌊n/2⌋)2⋅x=x2⌊n/2⌋+1=xn. Dans les deux cas P(n) est vraie, et par récurrence forte elle l'est pour tout n∈N. Coût. Notons C(n) le nombre de multiplications effectuées par l'appel sur l'exposant n. L'appel fait une multiplication (cas pair) ou deux (cas impair), plus le coût de l'unique appel sur ⌊n/2⌋ ; de plus demi n'est calculé qu'une fois, ce qui est exactement le point que l'écriture puissance(x, n//2) * puissance(x, n//2) ruinerait. On a donc C(0)=0 et, pour n≥1, C(n)≤C(⌊n/2⌋)+2. Par récurrence sur le nombre de chiffres binaires, C(n)≤2(⌊log2n⌋+1) pour n≥1 : en effet, si n≥2, alors ⌊log2⌊n/2⌋⌋=⌊log2n⌋−1, donc C(n)≤2⌊log2n⌋+2 ; et pour n=1, C(1)≤2. Ainsi C(n)=O(logn), ce que confirme le théorème maître avec a=1, b=2 et un travail hors appel constant (voir le thème Récursivité et diviser pour régner, où ce théorème est énoncé et appliqué). Profondeur de la pile. Le nombre d'appels emboîtés vaut ⌊log2n⌋+1 : pour n inférieur à 2900, la limite de récursion de Python n'est jamais atteinte. À retenir : dans une récursion « diviser pour régner », le coût s'écrit comme une relation de récurrence avant d'être résolu, et la ligne qui décide de tout est celle qui stocke le résultat de l'appel dans une variable au lieu de le recalculer.
Méthode
Calculer une puissance modulo m sans jamais former la puissance
Quand l'utiliser : l'énoncé demande xnmodm avec n grand : test de Fermat, chiffrement RSA, ordre d'un élément, dernier chiffre d'une puissance. Le signal : le mot « modulo » ou « les trois derniers chiffres de » accompagné d'un exposant à plusieurs chiffres. 1) Réduire dès l'initialisation : base = x % m, ce qui rend la fonction correcte même pour un x négatif ou énorme, et resultat = 1 % m, sans quoi un exposant nul renverrait 1 au lieu de 0 lorsque m vaut 1, la boucle ne tournant pas une seule fois. 2) Reprendre la boucle de l'exponentiation rapide en réduisant modulo maprès chaque multiplication, les deux, celle du résultat et l'élévation au carré. 3) Justifier la réduction par la compatibilité de la congruence avec le produit : si a≡a′(modm) et b≡b′(modm), alors ab≡a′b′(modm). C'est cette propriété, et rien d'autre, qui autorise à réduire en cours de route. 4) Constater le gain : tous les nombres manipulés restent inférieurs à m2, donc chaque multiplication est à coût constant tant que m tient sur un mot machine. 5) En production, préférer pow(x, n, m), qui est la même chose écrite en C ; en devoir, savoir écrire la boucle.
def puissance_modulaire(x, n, m):
"""Renvoie x ** n % m.
Precondition : n est un entier naturel et m >= 1."""
resultat = 1 % m
base = x % m
exposant = n
while exposant > 0:
if exposant % 2 == 1:
resultat = resultat * base % m
base = base * base % m
exposant = exposant // 2
return resultat
Contrôle : puissance_modulaire(7, 1000000, 13) et pow(7, 1000000, 13) renvoient tous deux 9.
a≡a′(modm)etb≡b′(modm)⟹ab≡a′b′(modm)
la compatibilité du produit avec la congruence, seule justification de la réduction
0≤R<m,0≤B<m,R⋅BE≡xn(modm)
l'invariant devient une congruence, et les variables restent bornées
Exemple
Rédaction modèle : les trois derniers chiffres d'une très grande puissance
Déterminer les trois derniers chiffres de 114321, sans jamais former cet entier. Mise en équation. Les trois derniers chiffres de l'écriture décimale d'un entier naturel N sont ceux de Nmod1000, complétés à gauche par des zéros si le reste a moins de trois chiffres. On cherche donc 114321mod1000, que l'on calcule par puissance_modulaire(11, 4321, 1000). Invariant. Notons Rk, Bk et Ek les valeurs de resultat, base et exposant après k tours, avec R0=1, B0=11 et E0=4321. L'invariant de l'exponentiation rapide devient une congruence, à laquelle s'ajoute l'encadrement que les réductions garantissent. Considérons Ik : « 0≤Rk<1000, 0≤Bk<1000 et Rk⋅BkEk≡114321(mod1000) ». Initialisation. On a R0⋅B0E0=114321, qui est congru à lui-même, et les deux encadrements sont vérifiés puisque 1 et 11 sont dans {0,…,999} : I0 est vraie. Hérédité. Supposons Ik vraie et le tour k+1 effectué. Posons m=⌊Ek/2⌋ et s=Ekmod2, de sorte que Ek=2m+s. Sans les réductions, le calcul mené dans la rédaction modèle de l'exponentiation rapide donne RkBks⋅(Bk2)m=Rk⋅BkEk. Les réductions ne changent rien à la classe de congruence : si a≡a′ et b≡b′ modulo 1000, alors ab≡a′b′ modulo 1000, et un entier est congru à son reste. Donc Rk+1⋅Bk+1Ek+1≡Rk⋅BkEk≡114321(mod1000). Enfin Rk+1 et Bk+1 sont des restes de divisions par 1000, donc compris entre 0 et 999 : Ik+1 est vraie. Conclusion. Notons K le nombre de tours. En sortie, EK=0, donc IK donne RK≡114321(mod1000) avec 0≤RK<1000 : RK est exactement le reste cherché. C'est l'encadrement, et non la seule congruence, qui autorise à conclure que la valeur renvoyée est le reste et non un quelconque entier congru. Trace. L'écriture binaire de 4321 est 1000011100001 : treize chiffres, dont cinq valent 1. La boucle fait donc treize tours et 13+5=18 multiplications. Avant la boucle, (R,B,E)=(1,11,4321). Après le premier tour, (11,121,2160) ; après le deuxième, (11,641,1080) ; après le troisième, (11,881,540). Les tours quatre et cinq laissent R inchangé et amènent (11,921,135). Le sixième donne (131,241,67), le septième (571,81,33), le huitième (251,561,16). Les quatre tours suivants ont un exposant pair et laissent R à 251, jusqu'à (251,961,1). Le treizième et dernier tour donne (211,521,0). Réponse. Les trois derniers chiffres de 114321 sont 211. Ordre de grandeur. L'entier 114321 compte ⌊4321log1011⌋+1=4500 chiffres décimaux, alors qu'aucun nombre manipulé par la boucle ne dépasse 9992, soit moins d'un million. C'est tout l'intérêt de la réduction en cours de route : le calcul tient sur des mots machine. Contrôle par le programme : puissance_modulaire(11, 4321, 1000) et pow(11, 4321, 1000) renvoient tous deux 211. À retenir : passer au modulo ne change pas l'invariant, il le transforme en congruence, et il faut lui adjoindre l'encadrement 0≤R<m pour pouvoir dire que la valeur renvoyée est le reste.
0≤Rk<m,0≤Bk<m,Rk⋅BkEk≡xn(modm)
l'invariant modulaire complet : la congruence ne suffit pas, l'encadrement fait la conclusion
Piège
Former la puissance entière avant de réduire modulo m
Ligne fautive : return puissance_rapide(x, n) % m, ou return x ** n % m. Le résultat est juste, mais le programme forme d'abord l'entier xn tout entier. Contre-exemple chiffré : 3100000 occupe 158497 bits, soit 47713 chiffres décimaux, alors que le résultat cherché modulo 7 tient sur un chiffre. Chaque multiplication de la boucle porte alors sur des entiers de plus en plus longs, et le nombre d'opérations élémentaires n'est plus Θ(logn) mais croît comme la taille des nombres manipulés. Pour un exposant de taille cryptographique, le calcul ne se termine pas, et la mémoire peut être épuisée avant. Version correcte : réduire à chaque étape, comme dans puissance_modulaire, ou appeler pow(x, n, m) à trois arguments. Sur ce dernier point, pow(x, n, m) et pow(x, n) % m ne sont pas deux écritures du même calcul : la première réduit en cours de route, la seconde non. Réflexe : dans un énoncé où le mot « modulo » apparaît, le modulo entre dans la boucle, il ne l'attend pas à la sortie.
Piège
Appeler l'exponentiation rapide avec un exposant négatif
Ligne fautive : puissance_rapide(2, -3), en espérant 0,125. La boucle while exposant > 0 n'est pas exécutée une seule fois, et la fonction renvoie 1, la valeur d'initialisation. Ce n'est ni une erreur levée, ni une boucle infinie : c'est un faux résultat, silencieux, qui se propagera dans tout le calcul. Le même phénomène frappe toute boucle dont la condition est fausse dès l'entrée et dont la variable de résultat a été initialisée à une valeur « neutre » plausible. Version correcte : écrire la précondition dans la docstring, et la faire respecter par une assertion, assert n >= 0, "exposant negatif", ou traiter explicitement le cas en renvoyant 1 / puissance_rapide(x, -n) quand la division a un sens. Règle générale de ce thème : toute fonction dont la boucle repose sur une décroissance d'entier naturel doit dire, dans sa spécification, que son argument est un entier naturel, et le vérifier.
Définition
Nombre premier
Un entier p est premier si p≥2 et si ses seuls diviseurs positifs sont 1 et p : ∀d∈N∗,(d∣p⟹d=1oud=p). Un entier n≥2 qui n'est pas premier est dit composé : il s'écrit alors n=d×e avec 1<d≤e<n. Les entiers 0 et 1 ne sont ni premiers ni composés ; 1 est exclu pour que la décomposition en facteurs premiers soit unique. Rappel hors ligne, en une phrase : les propriétés des nombres premiers (lemme d'Euclide, infinité, décomposition) sont établies dans le thème Nombres premiers et décomposition, bloc Algèbre 3 ; on s'en sert ici pour justifier des programmes, pas pour les redémontrer.
ppremier⟺p≥2et∀d∈N∗,(d∣p⟹d∈{1,p})
la définition, quantificateur compris
n≥2etnnon premier⟺∃(d,e)∈N2,n=deet1<d≤e<n
sa négation utile, celle qui fournit un facteur à exhiber
Propriété
Un entier composé admet un diviseur premier inférieur à sa racine carrée
Propriété : pour tout entier n≥2 composé, il existe un nombre premier p tel que p∣n et p≤n. Autrement dit : si n≥2 et si aucun entier d vérifiant 2≤d≤n ne divise n, alors n est premier. L'hypothèse n≥2 ne peut pas sauter : pour n=0 et pour n=1, l'ensemble des candidats {2,…,⌊n⌋} est vide, la condition est donc remplie sans qu'aucune division soit faite, et ni 0 ni 1 n'est premier. Démonstration : l'ensemble des diviseurs de n supérieurs ou égaux à 2 est une partie non vide de N (elle contient n), donc elle admet un plus petit élément p. Cet entier p est premier : si d≥2 divisait p, alors d diviserait n et d≤p, donc d=p par minimalité. Écrivons n=pm avec m∈N∗. Comme n est composé, p=n, donc m≥2 ; m est alors un diviseur de n supérieur ou égal à 2, donc m≥p par minimalité de p. Ainsi n=pm≥p2, donc p≤n. L'hypothèse « n composé » est indispensable : pour n=7, premier, aucun diviseur premier n'est inférieur ou égal à 7, et c'est précisément ce que le test de primalité exploite. C'est ce résultat qui fait passer le test de n divisions à environ n, et c'est lui qui autorise le crible à s'arrêter dès que p2 dépasse n.
n≥2etnnon premier⟹∃ppremier,p∣netp2≤n
le plus petit facteur d'un nombre composé est petit
n≥2et(∀d∈{2,…,⌊n⌋},d∤n)⟹npremier
la contraposée, hypothèse n≥2 comprise, qui est le critère effectivement programmé
Méthode
Tester la primalité par divisions jusqu'à la racine carrée
Quand l'utiliser : l'énoncé demande si un entier est premier, ou de compter les premiers d'un intervalle court, ou de trouver le premier nombre premier après une valeur donnée. Le signal : « déterminer si n est premier », « le plus petit diviseur de n ». 1) Traiter les cas limites avant la boucle : n<2 n'est pas premier ; n=2 l'est ; tout n pair autre que 2 ne l'est pas. Les oublier est l'erreur la plus fréquente, car la boucle qui suit suppose n impair. 2) Faire courir un diviseur candidat d sur les entiers impairs à partir de 3, avec un pas de 2 : la moitié des candidats est ainsi éliminée d'un coup. 3) Écrire la condition d'arrêt en entiers : while d * d <= n, jamais while d <= math.sqrt(n). 4) Renvoyer False dès qu'une division tombe juste : une fonction de test renvoie un booléen et sort immédiatement, elle n'accumule pas. 5) Justifier la correction par la propriété du plus petit facteur : si la boucle s'achève sans diviseur, aucun d vérifiant 2≤d≤n ne divise n, donc n n'est pas composé, donc il est premier. 6) Annoncer le coût : au plus 2n+1 divisions, soit O(n), atteint quand n est premier. Ce coût est exponentiel en la taille de n, c'est-à-dire en son nombre de chiffres : c'est pourquoi on utilise des tests probabilistes au-delà de quelques dizaines de chiffres.
d2≤ncontred≤n
la condition d'arrêt s'écrit avec des entiers, jamais avec un flottant
C(n)≤2n+1=O(n)
le nombre de divisions, atteint lorsque l'entier est premier
Exemple
Rédaction modèle : le test de primalité, sa correction et son coût
Écrire une fonction est_premier(n) qui renvoie True si l'entier n est premier et False sinon. Démontrer sa correction et donner son coût. Programme.
def est_premier(n):
"""Renvoie True si l'entier n est premier, False sinon.
Precondition : n est un entier."""
if n < 2:
return False
if n % 2 == 0:
return n == 2
d = 3
while d * d <= n:
if n % d == 0:
return False
d = d + 2
return True
Cas limites. Si n<2, l'entier n'est pas premier par définition, et la fonction renvoie False. Si n est pair, il est premier si et seulement s'il vaut 2, et n == 2 renvoie exactement cela. Après ces deux tests, n est un entier impair supérieur ou égal à 3. Terminaison. La variable d est un entier naturel qui augmente de 2 à chaque tour, donc d2 tend vers l'infini : la condition d * d <= n finit par être fausse. La boucle fait donc un nombre fini de tours, et en fait au plus 2n+1. Invariant. Notons dk la valeur de d après k tours, d0=3. Considérons Jk : « aucun entier impair d vérifiant 3≤d<dk ne divise n ». J0 est vraie, l'intervalle étant vide. Si le tour k+1 est effectué en entier, c'est que n % d_k != 0, donc dk ne divise pas n ; comme dk+1=dk+2, la propriété Jk+1 s'obtient en ajoutant ce seul entier impair à l'intervalle. Correction du cas False. Si la fonction sort par return False dans la boucle, c'est que dk divise n avec 3≤dk et dk2≤n, donc dk≤n<n : l'entier n admet un diviseur strictement compris entre 1 et n, il est composé. La réponse est juste. Correction du cas True. Si la boucle s'achève sans renvoyer False, notons K le nombre de tours et dK=3+2K la valeur finale de d ; la condition d'arrêt donne dK2>n, donc dK>n. Soit d un entier impair tel que 3≤d≤n. Alors d<dK, et comme d et dK sont tous deux impairs, d≤dK−2 : l'entier d a donc été testé dans la boucle, et JK garantit qu'il ne divise pas n. Par ailleurs n est impair, donc aucun entier pair ne divise n. Aucun entier de {2,…,⌊n⌋} ne divise donc n. D'après la propriété du plus petit facteur, si n était composé il admettrait un diviseur premier inférieur ou égal à n, ce qui vient d'être exclu. Donc n est premier, et la réponse True est juste. Coût. Les candidats testés sont les impairs de 3 à ⌊n⌋, soit au plus 2n divisions, auxquelles s'ajoute le test de parité : le coût est O(n) divisions, atteint pour n premier. Vérification : pour n=1000003, qui est premier, la fonction effectue 500 divisions, et 2n vaut environ 500 ; pour n=109+7, elle en effectue 15811. À retenir : un test de primalité se prouve en deux temps, la sortie négative par le diviseur exhibé, la sortie positive par la contraposée de la propriété du plus petit facteur, et les cas limites se traitent avant la boucle, jamais dedans.
Piège
Borner la boucle avec une racine carrée flottante
Ligne fautive : while d <= math.sqrt(n):, ou while d <= int(n ** 0.5):. Deux défauts, tous deux réels. D'abord, math.sqrt convertit son argument en flottant, ce qui ne conserve exactement que les entiers inférieurs à 253 : pour n=9999999999999999, int(math.sqrt(n)) vaut 100000000 alors que le plus grand entier de carré inférieur ou égal à n vaut 99999999, puisque 1000000002=1016>n : la borne est fausse. Ensuite, sur un entier vraiment grand, il n'y a plus de borne du tout : math.sqrt(10 ** 400 + 1) lève OverflowError: int too large to convert to float, et un programme d'arithmétique rencontre ce genre d'entier dès qu'il touche à la cryptographie. Accessoirement, écrire l'appel dans la condition le fait recalculer à chaque tour. Version correcte : while d * d <= n:, qui ne manipule que des entiers, reste exacte quelle que soit la taille de n et ne coûte qu'une multiplication. Si l'on tient à une borne explicite, math.isqrt(n), calculé une seule fois avant la boucle, renvoie la partie entière exacte de la racine carrée d'un entier, sans passer par les flottants.
Définition
Crible d'Ératosthène
Le crible d'Ératosthène est l'algorithme qui détermine d'un coup tous les nombres premiers inférieurs ou égaux à n. Il utilise un tableau de booléens premier de taille n+1, indexé par les entiers de 0 à n, initialisé à True partout sauf aux indices 0 et 1. On parcourt ensuite les entiers p croissants tant que p2≤n : si premier[p] vaut encore True, alors p est premier, et l'on met à False toutes les cases d'indice p2, p2+p, p2+2p, et ainsi de suite jusqu'à n. Deux choix méritent d'être compris plutôt que retenus. On démarre à p2 et non à 2p, parce que tout multiple pm avec 2≤m<p a déjà été rayé lors du passage de l'un des facteurs premiers de m, et que le seul multiple laissé de côté, celui de m=1, est p lui-même, qu'il ne faut surtout pas rayer. On s'arrête dès que p2>n, en vertu de la propriété du plus petit facteur : un entier composé au plus égal à n a été rayé par un premier inférieur ou égal à n. Le crible échange de la mémoire contre du temps : il occupe Θ(n) cases, mais trouve tous les premiers jusqu'à n bien plus vite que n tests de primalité indépendants.
marquage de {p2,p2+p,p2+2p,…}∩{0,…,n}
les indices rayés lors du passage d'un nombre premier
p2≤n
tant que le carré du candidat ne dépasse pas la borne, il reste des multiples à rayer ; au-delà, tout entier composé a déjà été rayé
Méthode
Programmer le crible d'Ératosthène avec un tableau de booléens
Quand l'utiliser : l'énoncé demande tous les nombres premiers jusqu'à une borne, ou de répondre à beaucoup de questions de primalité sur un même intervalle, ou de préparer une table de plus petits facteurs. Le signal : « la liste des nombres premiers inférieurs à », « combien de nombres premiers », « pour chaque entier de 2 à n ». 1) Écrire la spécification : « Renvoie la liste croissante des nombres premiers inférieurs ou égaux à n », précondition « n est un entier naturel ». 2) Créer le tableau premier = [True] * (n + 1), de taille n+1 pour que l'indice n existe, puis mettre les indices 0 et 1 à False en protégeant le second par un test si n peut valoir 0. 3) Écrire la boucle extérieure while p * p <= n, en entiers, et tester if premier[p] : on ne crible que si p est encore marqué premier, ce qui suffit, car un p déjà rayé n'apporte aucun multiple nouveau. 4) Écrire la boucle intérieure for multiple in range(p * p, n + 1, p) : départ à p2, pas p, et surtout borne n + 1, puisque range s'arrête un cran avant sa borne. 5) Construire la réponse par une liste en compréhension sur les indices encore marqués True. 6) Énoncer l'invariant de la boucle extérieure : « une case d'indice k vaut False si et seulement si k≤1 ou k admet un diviseur premier q<p avec q<k ». C'est de cet invariant que découle le fait, non évident, que premier[p] vaut True exactement quand p est premier, au moment où on le teste. 7) Annoncer le coût : Θ(n) en mémoire, et O(nloglogn) en temps.
{p2+kp:k∈N}∩{0,…,n}
les indices à rayer : départ au carré, pas égal au nombre premier, borne comprise
p≤n,ppremier∑pn∼nlnlnn
le nombre de marquages est majoré par cette somme, d'où le coût quasi linéaire
Exemple
Rédaction modèle : le crible, sa correction et son coût
Écrire une fonction crible(n) qui renvoie la liste croissante des nombres premiers inférieurs ou égaux à n, démontrer sa correction et donner son coût. Programme.
def crible(n):
"""Renvoie la liste croissante des nombres premiers inferieurs ou egaux a n.
Precondition : n est un entier naturel."""
premier = [True] * (n + 1)
premier[0] = False
if n >= 1:
premier[1] = False
p = 2
while p * p <= n:
if premier[p]:
for multiple in range(p * p, n + 1, p):
premier[multiple] = False
p = p + 1
return [k for k in range(n + 1) if premier[k]]
Invariant. Notons p la valeur courante de la variable p au début d'un tour de la boucle extérieure, et considérons la propriété Ip : « pour tout k∈{0,…,n}, la case premier[k] vaut False si et seulement si k≤1, ou il existe un nombre premier q<p tel que q divise k et q<k ». Initialisation. Pour p=2, aucun nombre premier n'est strictement inférieur à 2, et les seules cases mises à False sont celles d'indices 0 et 1 : I2 est vraie. Conséquence immédiate, à utiliser dans l'hérédité. Pour p≥2, l'invariant Ip appliqué à k=p dit que premier[p] vaut False si et seulement si p admet un diviseur premier q avec q<p, c'est-à-dire si et seulement si p est composé. Donc, au moment du test, premier[p] vaut True exactement lorsque p est premier. Hérédité. Supposons Ip vraie et le tour effectué. Premier cas : p est composé. D'après ce qui précède, premier[p] vaut False, aucune case n'est modifiée. Comme p n'est pas premier, la condition « il existe un premier q<p+1 » est équivalente à « il existe un premier q<p », donc Ip+1 est vraie. Second cas : p est premier. Les cases mises à False sont celles d'indices pm avec m≥p et pm≤n ; pour celles-là, p est bien un diviseur premier de l'indice, strictement plus petit que lui. Réciproquement, soit k≤n admettant p comme diviseur premier avec p<k : écrivons k=pm avec m≥2. Si m≥p, la case vient d'être mise à False. Si m<p, alors m admet un diviseur premier q≤m<p, et q divise k avec q<k, donc Ip garantit que la case valait déjà False. Dans tous les cas, Ip+1 est vraie. Par récurrence, Ip est vraie à chaque tour. Sortie de boucle. Notons P la valeur de p à la sortie : P2>n. Soit k∈{2,…,n}. Si k est composé, la propriété du plus petit facteur donne un diviseur premier q de k avec q≤k≤n<P ; de plus q<k puisque k est composé. Donc IP donne premier[k] égal à False. Si k est premier, aucun nombre premier q<k ne divise k, donc premier[k] vaut True. La liste renvoyée contient donc exactement les nombres premiers de {2,…,n}, par indices croissants. Coût. La mémoire occupée est Θ(n) booléens. Le nombre de marquages vaut au plus ∑p≤npn, et l'on admet l'estimation de Mertens ∑p≤np1∼lnlnn, qui donne un coût en O(nloglogn), quasi linéaire. Vérification numérique : pour n=106, le programme effectue 2122048 marquages, à comparer à nlnlnn≈2625792, et il renvoie 78498 nombres premiers. Trace sur n=30. Pour p=2, on raye 4,6,8,…,30. Pour p=3, on raye 9,12,15,18,21,24,27,30. Pour p=4, la case est déjà rayée, on passe. Pour p=5, on raye 25 et 30. Pour p=6, 36>30 arrête la boucle. Il reste 2,3,5,7,11,13,17,19,23,29. À retenir : l'invariant d'un crible ne porte pas sur le nombre courant mais sur tout le tableau, et c'est lui qui démontre le point que l'on croit évident, à savoir que la case testée en tête de tour est marquée True exactement pour les nombres premiers.
Piège
Laisser passer n parce que range s'arrête un cran avant
Ligne fautive : for multiple in range(p * p, n, p): dans le crible. La fonction range(a, b, pas) s'arrête avantb : l'indice n n'est jamais atteint, donc la case premier[n] n'est jamais rayée. Contre-exemple chiffré : avec cette version, crible(25) renvoie [2, 3, 5, 7, 11, 13, 17, 19, 23, 25], où 25=5×5 n'est pas premier ; la bonne version renvoie la même liste sans 25. Le défaut se voit dès que la borne n est elle-même composée, et alors toujours : aucune liste de multiples n'atteint l'indice n, quel que soit le facteur premier considéré, et le plus petit facteur de n n'y change rien. Le plus petit cas est crible(4), qui renvoie [2, 3, 4] au lieu de [2, 3], et crible(30) renvoie 30 en fin de liste. Seules les bornes premières, et les valeurs 0 et 1, masquent le bogue : les deux versions diffèrent exactement lorsque n est composé. Version correcte : range(p * p, n + 1, p), et un jeu de tests qui contient au moins une borne composée, 4, 25 ou 121 par exemple : c'est une borne première qui ne révèle rien, jamais une borne composée. La même vigilance vaut pour la ligne premier = [True] * (n + 1) : la taille est n+1, pas n, sans quoi l'indice n n'existe même pas.
Propriété
Théorème fondamental de l'arithmétique
Théorème : tout entier n≥2 s'écrit n=p1α1p2α2⋯prαr avec r≥1, p1<p2<⋯<pr des nombres premiers et α1,…,αr∈N∗ ; cette écriture est unique. L'exposant αi s'appelle la valuation pi-adique de n, notée vpi(n). Rappel hors ligne, en une phrase : existence et unicité sont démontrées dans le thème Nombres premiers et décomposition, bloc Algèbre 3, la première par récurrence forte et la seconde par le lemme d'Euclide. Ce théorème sert ici de spécification, au sens strict : c'est lui qui donne son sens au résultat renvoyé par une fonction de factorisation, et son unicité est ce qui autorise à comparer deux sorties. Deux conséquences immédiates guident les programmes : d divise n si et seulement si vp(d)≤vp(n) pour tout premier p, et vp(pgcd(a,b))=min(vp(a),vp(b)).
n=i=1∏rpiαi,p1<⋯<prpremiers,αi≥1
la décomposition, unique à l'ordre des facteurs près
ce que deviennent le PGCD et le produit une fois tout décomposé
Méthode
Décomposer un entier en facteurs premiers par divisions successives
Quand l'utiliser : l'énoncé demande la décomposition d'un entier, le nombre ou la somme de ses diviseurs, sa valuation p-adique, ou une simplification de fraction qui passe par les facteurs. Le signal : « décomposer », « les diviseurs de », « la plus grande puissance de p qui divise ». 1) Écrire la spécification et, surtout, décider du format de sortie : liste de couples (p, alpha) par nombres premiers croissants, ou liste de facteurs répétés. Les deux se rencontrent ; il faut choisir et l'écrire, sans quoi la fonction est intestable. 2) Tenir deux variables : reste, initialisé à n, qui porte ce qu'il reste à décomposer, et facteurs, la liste en construction. 3) Faire courir un candidat p à partir de 2 tant que p * p <= reste. La borne porte sur reste, qui rétrécit, et non sur n : c'est ce qui rend l'algorithme rapide sur les entiers très composés. 4) Quand p divise reste, le diviser autant de fois que possible par une boucle intérieure, en comptant l'exposant. Une seule division laisserait passer les carrés. 5) Ne pas tester la primalité de p : au moment où p divise reste, tous les nombres premiers plus petits ont déjà été épuisés, donc p est nécessairement premier. C'est une propriété à énoncer, pas une chance. 6) Après la boucle, traiter le reste : si reste > 1, c'est un nombre premier, qu'il faut ajouter avec l'exposant 1. C'est la ligne que les copies oublient. 7) Annoncer le coût : O(n) divisions dans le pire des cas, atteint quand n est premier, et bien moins dès que n a de petits facteurs.
n=(p,α)∈facteurs∏pα×reste
l'invariant de la boucle : rien ne se perd entre les facteurs sortis et ce qui reste
p2>resteetreste>1⟹resteest premier
pourquoi le dernier facteur s'ajoute sans avoir à être testé
Exemple
Rédaction modèle : la décomposition en facteurs premiers et le facteur restant
Écrire une fonction factoriser(n) qui renvoie la liste des couples (p,α) de la décomposition de n en facteurs premiers, par nombres premiers croissants, et démontrer sa correction. Programme.
def factoriser(n):
"""Renvoie la liste des couples (p, alpha) de la decomposition de n en
facteurs premiers, par nombres premiers p croissants.
Precondition : n est un entier superieur ou egal a 2."""
facteurs = []
reste = n
p = 2
while p * p <= reste:
if reste % p == 0:
alpha = 0
while reste % p == 0:
reste = reste // p
alpha = alpha + 1
facteurs.append((p, alpha))
p = p + 1
if reste > 1:
facteurs.append((reste, 1))
return facteurs
Invariant. Au début du tour où la variable p vaut p, considérons la propriété Ip : « n est égal au produit de reste par ∏piαi sur les couples déjà rangés dans facteurs ; ces couples sont exactement les (q,vq(n)) pour les nombres premiers q<p qui divisent n ; et aucun nombre premier q<p ne divise reste ». Initialisation. Pour p=2, la liste est vide, reste vaut n, et aucun nombre premier n'est strictement inférieur à 2 : I2 est vraie. Hérédité. Supposons Ip vraie. Si p ne divise pas reste, rien ne change, et comme aucun premier q<p ne divisait reste, aucun premier q<p+1 ne le divise : Ip+1 est vraie. Si p divise reste, alors p est premier : en effet, si p était composé, il admettrait un diviseur premier q<p, qui diviserait reste, ce que Ip interdit. La boucle intérieure divise reste par p tant que c'est possible ; elle s'arrête, car reste est un entier naturel non nul strictement décroissant à chaque division. À sa sortie, p ne divise plus reste, et le nombre de divisions effectuées est vp(reste), qui vaut vp(n) puisque, par Ip, les facteurs déjà extraits sont des nombres premiers différents de p. Le produit total est inchangé, car ce qui est retiré de reste est ajouté à la liste : Ip+1 est vraie. Terminaison. Posons V=reste−p2. À chaque tour de la boucle extérieure, p augmente de 1 et reste ne croît jamais, donc V décroît strictement. Tant que la boucle tourne, la condition p * p <= reste donne V≥0 : la suite des valeurs de V est une suite d'entiers naturels strictement décroissante, elle ne prend qu'un nombre fini de valeurs, et la boucle s'arrête. Sortie de boucle et facteur restant. Notons P la valeur finale de p : on a P2>reste, et par IP aucun nombre premier q<P ne divise reste. Si reste valait un entier composé, il admettrait, d'après la propriété du plus petit facteur, un diviseur premier q≤reste<P, ce qui est exclu. Donc reste vaut 1 ou est un nombre premier. Si reste vaut 1, la liste contient déjà tous les facteurs. Sinon, reste est un nombre premier q≥P, non encore rangé, et sa valuation dans n vaut exactement 1 : sans quoi q2 diviserait reste, donc reste≥q2≥P2, ce qui contredit P2>reste. La ligne facteurs.append((reste, 1)) complète donc la décomposition. Trace sur n=360. Pour p=2, la boucle intérieure divise trois fois : reste passe à 45 et l'on range (2,3). Pour p=3, elle divise deux fois : reste passe à 5 et l'on range (3,2). Pour p=4, 16>5 arrête la boucle extérieure. Comme reste vaut 5, strictement supérieur à 1, on ajoute (5,1). Résultat : [(2,3),(3,2),(5,1)], et l'on vérifie 23×32×5=8×9×5=360. À retenir : l'invariant d'une factorisation dit que le produit de ce qui est sorti par ce qui reste vaut toujours n, et c'est la sortie de boucle, comparant p2 à reste, qui démontre que le dernier facteur est premier sans avoir à le tester.
Piège
Oublier le facteur premier resté après la boucle
Ligne fautive : une fonction factoriser qui se termine par return facteurs sans la ligne if reste > 1: facteurs.append((reste, 1)). La boucle s'arrête dès que p2>reste, donc le dernier facteur premier, qui est plus grand que la racine de ce qu'il reste, n'est jamais extrait. Contre-exemples chiffrés : cette version renvoie [(2, 1)] pour 14 au lieu de [(2, 1), (7, 1)], la liste vide pour 97 au lieu de [(97, 1)], et [(2, 3), (3, 2)] pour 360 au lieu de [(2, 3), (3, 2), (5, 1)]. L'erreur se propage sans bruit dans tout ce qui suit : un compte de diviseurs, une somme, une réduction de fraction. Version correcte : conserver la ligne finale, et surtout tester la spécification, pas la sortie. Le bon contrôle tient en trois lignes : pour chaque n d'un intervalle, reconstituer ∏pα à partir de la liste renvoyée et vérifier que le produit vaut n, en vérifiant au passage que chaque p est premier. Une fonction de factorisation testée seulement sur 360 et 100 passe à côté de tous les entiers ayant un grand facteur premier, c'est-à-dire de presque tous.
Définition
Polynôme représenté par la liste de ses coefficients
On représente le polynôme P=∑i=0daiXi par la liste P de ses coefficients rangés par degrés croissants : P[i] est le coefficient de Xi. Ainsi [1, 2, 3] représente 1+2X+3X2, et [0, 1] représente X. Le polynôme nul est représenté par la liste vide []. Une liste est dite normalisée si elle est vide ou si son dernier coefficient est non nul. Pour une liste normalisée, le degré vaut len(P) - 1, et l'on convient de renvoyer −1 pour le polynôme nul, sentinelle qui remplace le −∞ des mathématiques (voir le thème Polynômes : anneau K[X] et division euclidienne, bloc Algèbre 1). Toute fonction de ce thème renvoie une liste normalisée, ce qui rend le degré lisible et l'égalité de deux polynômes testable par P == Q. Le choix des degrés croissants n'est pas neutre : il fait de l'indice l'exposant, donc P[i + j] reçoit le produit P[i] * Q[j] sans aucune conversion.
P=i=0∑daiXi⟷[a0,a1,…,ad]
l'indice de la liste est l'exposant de l'indéterminée
degP=max{i:ai=0},deg0=−1par convention de programmation
le degré se lit sur la liste normalisée, avec une sentinelle pour le polynôme nul
Méthode
Additionner et multiplier deux polynômes coefficient par coefficient
Quand l'utiliser : l'énoncé demande de manipuler des polynômes en machine, de composer des suites récurrentes, de calculer un produit de séries tronquées ou une puissance de polynôme. Le signal : « on représente un polynôme par la liste de ses coefficients ». 1) Écrire d'abord une fonction normaliser qui retire les zéros de plus haut degré, et une fonction degre qui en déduit le degré. Toutes les autres fonctions s'appuient dessus, et le degré ne se lit jamais directement sur len. 2) Pour la somme : créer une liste de zéros de longueur max(len(P), len(Q)), ajouter les coefficients de P puis ceux de Q, et normaliser. Dimensionner la liste de travail sur les longueurs des deux listes, et non sur leurs degrés : les deux quantités coïncident quand les listes sont normalisées, mais une liste qui traîne des zéros de plus haut degré, comme [1, 2, 0], est plus longue que son degré ne le laisse croire, et la place manquerait. Deux boucles séparées, et non une boucle unique avec des tests d'indice : c'est plus court et il n'y a plus d'IndexError possible. 3) Ne jamais partir de R = P : cette ligne n'est pas une copie, elle donne un second nom à la même liste, et modifier R modifie l'argument de l'appelant (voir le thème Python : listes, tuples, chaînes et dictionnaires). 4) Pour le produit : traiter d'abord le cas où l'un des deux est nul, puis créer une liste de zéros de longueur len(P) + len(Q) - 1 et accumuler R[i + j] += P[i] * Q[j] dans une double boucle. Là encore la longueur se lit sur les listes : le plus grand indice atteint est len(P) - 1 + len(Q) - 1, que les coefficients de plus haut degré soient nuls ou non. C'est la formule du produit de Cauchy, écrite telle quelle. 5) Justifier la correction par cette formule : le coefficient de Xk dans PQ vaut ∑i+j=kaibj, et la double boucle visite chaque couple (i,j) exactement une fois. 6) Annoncer les coûts : la somme est en Θ(max(n,m)) opérations, le produit en Θ(nm) multiplications pour des degrés n et m.
(i∑aiXi)(j∑bjXj)=k∑i+j=k∑aibjXk
le produit de Cauchy, qui est littéralement la double boucle
deg(PQ)=degP+degQsiP=0etQ=0
le degré du produit, qui donne la longueur de la liste de travail pour des listes normalisées, et le cas particulier à traiter à part
Exemple
Rédaction modèle : somme et produit de deux polynômes, correction et coût
Les polynômes étant représentés par la liste de leurs coefficients par degrés croissants, écrire les fonctions normaliser, degre, somme_poly et produit_poly, justifier la correction du produit et donner les coûts. Programme.
def normaliser(P):
"""Renvoie la liste des coefficients de P privee de ses zeros de plus haut
degre. Le polynome nul est represente par la liste vide."""
Q = list(P)
while Q != [] and Q[-1] == 0:
Q.pop()
return Q
def degre(P):
"""Renvoie le degre du polynome P, et -1 si P est le polynome nul."""
return len(normaliser(P)) - 1
def somme_poly(P, Q):
"""Renvoie la liste normalisee des coefficients de P + Q."""
R = [0] * max(len(P), len(Q))
for i in range(len(P)):
R[i] = R[i] + P[i]
for j in range(len(Q)):
R[j] = R[j] + Q[j]
return normaliser(R)
def produit_poly(P, Q):
"""Renvoie la liste normalisee des coefficients de P * Q."""
if normaliser(P) == [] or normaliser(Q) == []:
return []
R = [0] * (len(P) + len(Q) - 1)
for i in range(len(P)):
for j in range(len(Q)):
R[i + j] = R[i + j] + P[i] * Q[j]
return normaliser(R)
Correction du produit. Notons P=∑i=0naiXi et Q=∑j=0mbjXj, où n et m sont les longueurs diminuées de 1. Le produit s'écrit PQ=∑i=0n∑j=0maibjXi+j par distributivité, puis, en regroupant selon k=i+j, PQ=∑k=0n+m(∑i+j=kaibj)Xk. La liste R a pour longueur n+m+1, donc l'indice i+j y est toujours valide, puisque i+j≤n+m. La double boucle parcourt chaque couple (i,j) de {0,…,n}×{0,…,m} exactement une fois et ajoute aibj à la case i+j. À la fin, R[k] contient donc exactement ∑i+j=kaibj, c'est-à-dire le coefficient de Xk dans PQ. Le cas où l'un des deux polynômes est nul est traité avant. Ce n'est pas qu'une erreur serait levée : en Python [0] * (-1) vaut [], et la fonction privée de sa garde renverrait encore la liste vide, qui est la bonne réponse. C'est que le raisonnement ci-dessus lit n et m comme des degrés, ce qui suppose les deux listes non vides ; la garde écarte d'avance le cas où la longueur len(P) + len(Q) - 1 serait négative, et elle dit l'intention au lieu de la laisser deviner. Coûts. La somme effectue len(P) + len(Q) additions, donc Θ(max(n,m)) opérations. Le produit effectue exactement (n+1)(m+1) multiplications et autant d'additions, donc Θ(nm) opérations : pour deux polynômes de degré 3 et 4, cela fait 20 multiplications. Vérifications. somme_poly([1, 2, 3], [0, 1]) renvoie [1, 3, 3], c'est-à-dire 1+3X+3X2. produit_poly([1, 2, 3], [0, 1]) renvoie [0, 1, 2, 3], c'est-à-dire X+2X2+3X3, ce qui est bien X(1+2X+3X2). produit_poly([-1, 1], [1, 1]) renvoie [-1, 0, 1], c'est-à-dire X2−1=(X−1)(X+1). Enfin somme_poly([1, 2], [3, -2]) renvoie [4] : la normalisation a supprimé le terme en X devenu nul, et le degré tombe de 1 à 0. À retenir : la double boucle du produit est la formule de Cauchy, et la normalisation en sortie est ce qui rend le degré et l'égalité de deux polynômes fiables.
Propriété
Schéma de Horner
Propriété : pour tout polynôme P=∑i=0naiXi et tout x, P(x)=a0+x(a1+x(a2+⋯+x(an−1+xan)⋯)). Cette écriture, appelée schéma de Horner, s'obtient par factorisations successives de x dans la somme, de la droite vers la gauche. Elle se lit comme un algorithme : on part du coefficient de plus haut degré, et l'on répète « multiplier par x, ajouter le coefficient suivant ». Coût : n multiplications et n additions pour un polynôme de degré n, soit Θ(n) opérations. L'évaluation naïve qui recalcule xi terme à terme en demande 2n(n+1), soit Θ(n2) : on compte i−1 produits répétés pour former xi, plus une multiplication par le coefficient, soit i multiplications pour le terme d'indice i. Pour n=100, cela fait 100 multiplications contre 5050. Horner présente un second avantage, qu'on ne démontre pas ici : en arithmétique flottante, il évite de former les grandes puissances xi, dont l'addition avec des termes petits est la source principale d'erreur d'arrondi.
P(x)=a0+x(a1+x(a2+⋯+x(an−1+xan)⋯))
le polynôme factorisé en cascade, du plus haut degré vers le plus bas
v←0,v←vx+aipouri=n,n−1,…,0
l'algorithme en une ligne : multiplier, ajouter, redescendre d'un indice
Méthode
Évaluer un polynôme par le schéma de Horner
Quand l'utiliser : partout où l'on évalue un polynôme, et en particulier dans une boucle qui l'évalue en beaucoup de points, ou quand les coefficients sont flottants. Le signal : « calculer P(x) », « tracer la courbe de P », « évaluer en n points ». 1) Initialiser une variable valeur à 0, et non au coefficient dominant : partir de 0 rend la fonction correcte pour la liste vide, qui représente le polynôme nul. 2) Parcourir les indices par degrés décroissants : for i in range(len(P) - 1, -1, -1). Les trois arguments ne s'improvisent pas : départ au dernier indice, borne −1 exclue pour atteindre 0, pas −1. 3) Écrire le corps en une ligne, valeur = valeur * x + P[i], dans cet ordre : multiplier d'abord, ajouter ensuite. 4) Énoncer l'invariant, qui est ce que le correcteur attend : après le traitement de l'indice i, valeur vaut ∑j=inajxj−i. 5) Conclure : pour i=0, l'invariant donne ∑j=0najxj=P(x). 6) Ne jamais écrire valeur += P[i] * x ** i dans une boucle croissante : c'est correct mais quadratique, et sur des flottants c'est en plus moins précis.
vi=j=i∑najxj−i
l'invariant de Horner, écrit après le traitement de l'indice i
vi=vi+1x+ai
l'hérédité tient en une égalité, celle qu'écrit la ligne de code
Exemple
Rédaction modèle : Horner, son invariant et son coût
Écrire une fonction horner(P, x) qui évalue en x le polynôme représenté par la liste P, démontrer sa correction et donner son coût. Programme.
def horner(P, x):
"""Renvoie la valeur P(x) du polynome P au point x."""
valeur = 0
for i in range(len(P) - 1, -1, -1):
valeur = valeur * x + P[i]
return valeur
Notations. Posons P=∑j=0najXj avec aj le coefficient P[j] et n l'indice du dernier coefficient, c'est-à-dire len(P) - 1. Notons vi la valeur de valeur juste après le traitement de l'indice i, et vn+1=0 la valeur avant le premier tour. Invariant. Montrons par récurrence descendante sur i, de n+1 à 0, la propriété Hi : « vi=∑j=inajxj−i ». Initialisation. Pour i=n+1, la somme est vide donc nulle, et vn+1=0 : Hn+1 est vraie. Hérédité. Soit i∈{0,…,n} ; supposons Hi+1 vraie. La ligne de la boucle donne vi=vi+1x+ai, donc vi=x∑j=i+1najxj−i−1+ai=∑j=i+1najxj−i+aix0=∑j=inajxj−i. Donc Hi est vraie, et par récurrence descendante Hi l'est pour tout i de n+1 à 0. Conclusion. La boucle se termine après avoir traité l'indice 0, et H0 donne v0=∑j=0najxj=P(x) : la valeur renvoyée est bien P(x). Pour la liste vide, la boucle ne tourne pas et la fonction renvoie 0, ce qui est la valeur du polynôme nul. Coût. La boucle fait n+1 tours, chacun comportant une multiplication et une addition, mais le tout premier tour multiplie 0 par x et ajoute an à 0 : ces deux opérations ne servent à rien. Le coût est donc de n multiplications et n additions utiles, soit Θ(n) opérations, à comparer aux 2n(n+1) multiplications de l'évaluation naïve. Trace sur P=2X3+4X2−1, soit P = [-1, 0, 4, 2], en x=3. La variable valeur vaut 0 avant la boucle, puis 2 après l'indice 3, puis 2×3+4=10 après l'indice 2, puis 10×3+0=30 après l'indice 1, puis 30×3−1=89 après l'indice 0. Et 2×27+4×9−1=54+36−1=89. À retenir : l'invariant de Horner n'est pas « valeur vaut un morceau de P », mais la somme décalée∑j≥iajxj−i ; c'est le décalage qui fait que la multiplication par x du tour suivant remet chaque terme à sa place.
Piège
Lire le degré comme la longueur de la liste diminuée de un
Ligne fautive : d = len(P) - 1 pour obtenir le degré de P. Cela n'est vrai que si la liste est normalisée, c'est-à-dire si son dernier coefficient est non nul. Contre-exemple chiffré : A = [1, 1, 0] représente 1+X, de degré 1, quand la formule fautive annonce 2 ; avec B = [1, 1], elle annonce donc deg(AB)=2+1=3, alors que AB=(1+X)2=1+2X+X2 est de degré 2, et que le produit calculé renvoie bien [1, 2, 1]. L'erreur se propage dans tout raisonnement sur les degrés : division euclidienne, test d'égalité, condition d'arrêt d'une récursion. Version correcte : normaliser avant de lire le degré, et faire renvoyer −1 pour le polynôme nul. Concrètement, degre(P) est len(normaliser(P)) - 1, et toute fonction qui construit un polynôme renvoie une liste normalisée. Le test P == Q pour l'égalité de deux polynômes suppose lui aussi la normalisation : sans elle, [1, 0] et [1] représentent le même polynôme et ne sont pas égaux comme listes.
Piège
Donner un second nom à la liste au lieu de la copier
Ligne fautive :
def somme_poly(P, Q):
R = P
for i in range(len(Q)):
R[i] = R[i] + Q[i]
return R
L'affectation R = P ne copie rien : elle donne un second nom à la même liste. Modifier R modifie donc le polynôme de l'appelant. Contre-exemple chiffré : avec P = [1, 2, 3] et Q = [0, 1, 0], l'appel S = somme_poly(P, Q) donne bien S = [1, 3, 3], mais P vaut désormais [1, 3, 3] lui aussi, et P is S renvoie True. Un second appel somme_poly(P, Q) ajouterait Q une fois de plus, et tout calcul ultérieur utilisant P serait faux sans qu'aucune erreur soit levée. Version correcte : construire une liste neuve, R = [0] * max(len(P), len(Q)), puis y ajouter les deux polynômes. Si l'on tient à partir de P, écrire R = list(P), qui en fait une copie. La règle générale de la mutabilité et des alias est traitée dans le thème Python : listes, tuples, chaînes et dictionnaires ; ce qu'il faut en retenir ici est qu'une fonction sur les polynômes ne modifie jamais ses arguments, et que sa spécification doit le dire.
Piège
Recalculer la puissance de x à chaque terme de la somme
Ligne fautive : valeur = valeur + P[i] * x ** i dans une boucle croissante sur i. Le résultat est juste, mais chaque x ** i recommence un calcul de puissance. En comptant i−1 produits répétés pour former xi, plus une multiplication par le coefficient, le terme d'indice i coûte i multiplications et le total vaut 1+2+⋯+n=2n(n+1), donc quadratique en le degré. Contre-exemple chiffré : pour n=100, cette écriture demande 5050 multiplications quand Horner en demande 100. Sur une boucle qui évalue le même polynôme en des milliers de points, le facteur se paie en entier. Et la variante « améliorée » qui conserve une variable puissance multipliée par x à chaque tour reste plus coûteuse que Horner d'une multiplication par terme, en plus d'être moins précise sur des flottants. Version correcte : le schéma de Horner, qui parcourt les indices par degrés décroissants et n'effectue qu'une multiplication et une addition par coefficient. Réflexe de relecture : une puissance calculée à l'intérieur d'une boucle dont l'indice est l'exposant est presque toujours le signe d'un coût quadratique caché.
Piège
Comparer deux évaluations flottantes avec l'égalité
Ligne fautive : if horner(P, x) == valeur_attendue: avec des coefficients flottants. Les flottants ne représentent pas exactement les décimaux : contre-exemple chiffré, horner([0.1, 0.2], 3) renvoie 0.7000000000000001, si bien que la comparaison avec 0.7 est fausse, alors que le polynôme 0,1+0,2X vaut bien 0,7 en 3. Le test ne détecte donc pas une erreur du programme, il détecte un arrondi. Le même piège frappe un test de racine, if horner(P, x) == 0, qui échoue presque toujours sur des données flottantes. Version correcte : comparer à une tolérance, abs(horner(P, x) - valeur_attendue) <= 1e-9, ce qui renvoie True sur l'exemple ci-dessus ; ou, mieux, travailler en exact quand l'énoncé le permet, avec des coefficients entiers ou des Fraction du module fractions. Tous les algorithmes arithmétiques de ce thème, eux, ne manipulent que des entiers : c'est précisément ce qui les rend exacts, et c'est la raison pour laquelle on écrit d * d <= n plutôt qu'une racine carrée.
Formulaire
Les énoncés du thème réunis
Les résultats sur lesquels reposent les programmes de ce thème, dans l'ordre où on les invoque. Chacun est démontré dans une carte précédente, sauf ceux qui sont explicitement admis (théorème de Lamé, estimation de Mertens, division euclidienne, théorème de Bézout et théorème fondamental de l'arithmétique, ces trois derniers venant du bloc Algèbre 3).
∀a∈Z,∀b∈Z∗,∃!(q,r),a=bq+ret0≤r<∣b∣
division euclidienne, socle de tout le thème
pgcd(a,b)=pgcd(b,amodb),pgcd(m,0)=∣m∣
invariant d'Euclide, et sa valeur en sortie de boucle
∃(u,v)∈Z2,au+bv=pgcd(a,b)
Bézout, calculé par l'algorithme d'Euclide étendu
pgcd(a,b)∣c
condition d'existence d'une solution entière
xn=(x⌊n/2⌋)2xnmod2,R⋅BE=xn
relation et invariant de l'exponentiation rapide
n≥2etnnon premier⟹∃ppremier,p∣netp2≤n
borne du test de primalité, du crible et de la factorisation
n=i=1∏rpiαi,p1<⋯<pr
théorème fondamental de l'arithmétique, spécification de la factorisation
(PQ)k=i+j=k∑aibj,P(x)=a0+x(a1+x(a2+⋯))
produit de Cauchy et schéma de Horner
Formulaire
Les coûts des algorithmes du thème
Coûts à connaître par cœur, exprimés en nombre d'opérations arithmétiques sur des entiers tenant sur un mot machine ; n désigne l'entier traité, ou le degré pour les polynômes. Deux d'entre eux sont logarithmiques en la valeur (Euclide, exponentiation rapide), deux sont en racine de la valeur (test de primalité, factorisation par divisions successives), donc exponentiels en le nombre de chiffres, et c'est cet écart qui fait toute la cryptographie : multiplier deux nombres premiers de trois cents chiffres est instantané, retrouver les facteurs ne l'est pas.
O(logmin(∣a∣,∣b∣))
algorithme d'Euclide, et Euclide étendu : nombre de divisions euclidiennes, majoré par deux fois le logarithme en base deux du plus petit argument, plus deux
Θ(logn)
exponentiation rapide : nombre de multiplications, contre n moins une pour la boucle naïve
O(n)
test de primalité par divisions et décomposition en facteurs premiers : nombre de divisions dans le pire des cas, atteint lorsque l'entier est premier
O(nloglogn)
crible d'Ératosthène : quasi linéaire en temps, pour une mémoire linéaire en la borne
Θ(max(n,m))etΘ(nm)
somme puis produit de deux polynômes de degrés n et m : additions d'un côté, multiplications de l'autre
Θ(n)
schéma de Horner : n multiplications et n additions, contre un coût quadratique pour l'évaluation naïve
Exercices
Exercice 1Application
Trois traces à donner exactement
Les trois programmes qui suivent sont indépendants. Pour chacun, donner exactement les lignes affichées, puis contrôler le résultat final par un calcul mené à la main. 1) Programme A.
def trace_pgcd(a, b):
tours = 0
while b != 0:
a, b = b, a % b
tours = tours + 1
print(a, tours)
trace_pgcd(858, 253)
2) Programme B.
def trace_puissance(x, n):
resultat, base, exposant = 1, x, n
while exposant > 0:
if exposant % 2 == 1:
resultat = resultat * base
base = base * base
exposant = exposant // 2
print(resultat, base, exposant)
trace_puissance(2, 11)
3) Programme C. La liste P représente le polynôme dont le coefficient de Xi est P[i].
def trace_horner(P, x):
valeur = 0
for i in range(len(P) - 1, -1, -1):
valeur = valeur * x + P[i]
print(i, valeur)
trace_horner([4, -3, 0, 2], -1)
4) Pour le programme A, la majoration du cours annonce au plus 2log2(253)+2 divisions euclidiennes. Comparer avec le nombre de tours réellement effectués.
Voir le corrigé
1) Le programme A n'affiche qu'une ligne, après la boucle.
11 5
Les couples (a, b) successifs sont (858,253), puis (253,99) car 858=3×253+99, puis (99,55) car 253=2×99+55, puis (55,44) car 99=1×55+44, puis (44,11) car 55=1×44+11, et enfin (11,0) car 44=4×11. La boucle a donc effectué cinq tours, et la variable a vaut 11 au moment de l'affichage. Contrôle à la main : 858=2×3×11×13 et 253=11×23. Le seul facteur premier commun est 11, affecté de l'exposant 1 dans les deux décompositions, donc pgcd(858,253)=11.
2) Le programme B affiche une ligne par tour de boucle, et l'affichage a lieu en fin de tour.
2 4 5
8 16 2
8 256 1
2048 65536 0
L'exposant 11 s'écrit 1011 en binaire, ce qui annonce quatre tours. Premier tour : l'exposant est impair, donc resultat passe de 1 à 2 ; la base devient 4 et l'exposant 5. Deuxième tour : 5 est impair, donc resultat devient 2×4=8 ; la base devient 16 et l'exposant 2. Troisième tour : 2 est pair, resultat ne bouge pas, la base devient 256 et l'exposant 1. Quatrième tour : 1 est impair, donc resultat devient 8×256=2048 ; la base devient 65536 et l'exposant 0, ce qui arrête la boucle. Contrôle à la main : 211=2048. On remarque au passage que la dernière élévation au carré ne sert à rien, puisque la base n'est plus lue ensuite.
3) Le programme C affiche une ligne par indice parcouru, du dernier au premier.
3 2
2 -2
1 -1
0 5
La liste [4, -3, 0, 2] représente P=4−3X+2X3. Indice 3 : valeur vaut 0×(−1)+2=2. Indice 2 : 2×(−1)+0=−2. Indice 1 : (−2)×(−1)+(−3)=−1. Indice 0 : (−1)×(−1)+4=5. Contrôle à la main : P(−1)=4−3×(−1)+2×(−1)3=4+3−2=5.
4) On a 2log2(253)+2 qui vaut environ 17,97, donc la majoration annonce au plus 17 divisions, alors que le programme n'en effectue que 5. La majoration n'est pas mauvaise pour autant : elle ne prétend pas donner le nombre de tours, seulement un ordre de grandeur valable pour tous les couples, et elle n'est approchée qu'au voisinage du pire cas, celui de deux termes consécutifs de la suite de Fibonacci.
Exercice 2Application
Le plus petit facteur premier, et tout ce qu'il donne
1) Écrire une fonction plus_petit_facteur(n) qui renvoie le plus petit diviseur premier de n, avec sa docstring et sa précondition. La condition d'arrêt de la boucle ne doit contenir que des entiers, sans racine carrée flottante. 2) En déduire une fonction est_premier(n) qui accepte un entier quelconque, y compris négatif ou nul. 3) Donner, sans machine, la valeur de plus_petit_facteur(91), de plus_petit_facteur(97) et de plus_petit_facteur(1001), puis celle de est_premier(n) pour n=−7, n=0, n=1 et n=2. 4) Écrire factoriser(n) qui renvoie, pour n≥2, la liste des couples (p, alpha) de la décomposition de n en facteurs premiers, par nombres premiers croissants, en appelant plus_petit_facteur. Donner sa valeur pour n=2024. 5) Majorer le nombre de divisions effectuées par plus_petit_facteur(n), puis donner le nombre exact de tours de boucle pour n=1000003, qui est premier.
Voir le corrigé
1) On traite le cas pair avant la boucle, ce qui permet ensuite de n'avancer que de deux en deux.
def plus_petit_facteur(n):
"""Renvoie le plus petit diviseur premier de n.
Precondition : n est un entier superieur ou egal a 2."""
if n % 2 == 0:
return 2
d = 3
while d * d <= n:
if n % d == 0:
return d
d = d + 2
return n
La valeur renvoyée est bien un nombre premier, et c'est bien le plus petit facteur premier, dans chacun des trois cas de sortie. Sortie par return 2 : l'entier 2 est premier et divise n, et aucun entier strictement compris entre 1 et 2 ne divise quoi que ce soit. Sortie par return d dans la boucle : l'entier d divise n. Il est premier, car s'il admettait un diviseur premier q vérifiant 2≤q<d, alors q diviserait aussi n ; or q n'est pas 2 puisque n est impair à ce stade, et un q impair strictement inférieur à d aurait été testé lors d'un tour antérieur, ce qui aurait provoqué un retour plus tôt. C'est aussi le plus petit, par le même argument. Sortie par return n : aucun entier d vérifiant 2≤d≤n ne divise n, les pairs étant exclus parce que n est impair et les impairs ayant tous été testés. D'après la propriété du plus petit facteur, n n'est pas composé, donc il est premier, et son plus petit diviseur premier est lui-même.
2) Il suffit de comparer le plus petit facteur à l'entier lui-même.
def est_premier(n):
"""Renvoie True si l'entier n est premier, False sinon.
Precondition : n est un entier."""
return n >= 2 and plus_petit_facteur(n) == n
L'ordre des deux membres du and n'est pas indifférent : Python évalue le second seulement si le premier est vrai, donc plus_petit_facteur n'est jamais appelée sur un entier inférieur à 2, et sa précondition est respectée. Dans l'ordre inverse, la fonction renverrait encore les bonnes réponses sur les entiers demandés, mais elle ferait tourner la boucle sur des données qu'elle n'est pas censée recevoir : plus_petit_facteur(-7) renvoie −7, qui n'est pas un nombre premier, et c'est le and qui rattrape le résultat après coup. Ajouter dans plus_petit_facteur l'assertion assert n >= 2, ce que sa précondition invite à faire, rendrait l'ordre inverse franchement fautif.
3) Comme 91=7×13 et que ni 3 ni 5 ne divisent 91, on a plus_petit_facteur(91) qui vaut 7. Pour 97, les candidats testés sont 3, 5, 7 et 9, aucun ne divise 97, et le candidat suivant 11 vérifie 121>97 : la fonction renvoie 97. Comme 1001=7×11×13, elle renvoie 7. Enfin est_premier(-7), est_premier(0) et est_premier(1) valent False par le premier membre du and, et est_premier(2) vaut True.
4) On extrait le plus petit facteur, on le retire autant de fois que possible, et l'on recommence sur ce qu'il reste.
def factoriser(n):
"""Renvoie la liste des couples (p, alpha) de la decomposition de n en
facteurs premiers, par nombres premiers croissants.
Precondition : n est un entier superieur ou egal a 2."""
facteurs = []
reste = n
while reste > 1:
p = plus_petit_facteur(reste)
alpha = 0
while reste % p == 0:
reste = reste // p
alpha = alpha + 1
facteurs.append((p, alpha))
return facteurs
Les nombres premiers sortent bien par valeurs croissantes : après extraction complète de p, le reste n'a plus aucun facteur premier inférieur ou égal à p, donc le plus petit facteur du reste est strictement plus grand que p. La boucle extérieure termine parce que reste est un entier naturel qui est divisé par un entier supérieur ou égal à 2 à chaque passage, donc qui décroît strictement. Comme 2024=23×11×23, l'appel factoriser(2024) renvoie [(2, 3), (11, 1), (23, 1)]. Contrôle :
5) La boucle ne teste que les entiers impairs 3,5,7,… tant que d2≤n, donc tant que d≤n : elle fait au plus 2n−3+1 tours, soit O(n) divisions, auxquelles s'ajoute la division par 2 effectuée avant la boucle. Ce maximum est atteint lorsque n est premier, puisque la fonction ne sort alors jamais par anticipation. Pour n=1000003, la partie entière de la racine vaut 1000, et les candidats sont les impairs de 3 à 999, soit 2999−3+1=499 tours de boucle, chacun comportant une division. Avec le test de parité, cela fait 500 divisions, à comparer au million de divisions qu'un parcours de tous les entiers inférieurs à n demanderait.
Exercice 3Classique
Euclide étendu sur 154 et 35 : Bézout, une équation, un inverse
L'algorithme d'Euclide étendu fait tourner deux triplets (r,u,v) et (r′,u′,v′), initialisés respectivement à (a,1,0) et (b,0,1). À chaque tour, il calcule le quotient q de r par r′, puis remplace ce couple de triplets par (r′,u′,v′) et (r−qr′,u−qu′,v−qv′) ; il s'arrête lorsque le reste du second triplet est nul, et renvoie le premier triplet. 1) Dérouler cet algorithme sur a=154 et b=35 : donner, pour chaque tour, le quotient utilisé et les deux triplets obtenus, puis le triplet renvoyé. Vérifier l'égalité de Bézout obtenue. 2) Résoudre dans Z2 l'équation 154u+35v=21 : justifier l'existence de solutions, en exhiber une, puis décrire l'ensemble de toutes les solutions par analyse et synthèse. 3) L'entier 35 est-il inversible modulo 154 ? Justifier. 4) Démontrer que 23 est inversible modulo 154, déterminer son inverse dans {0,…,153} et vérifier le résultat par un calcul.
Voir le corrigé
1) Au départ, les deux triplets sont (154,1,0) et (35,0,1), qui traduisent les deux écritures évidentes 154=154×1+35×0 et 35=154×0+35×1. Tour 1 : q=4 puisque 154=4×35+14. Les triplets deviennent (35,0,1) et (154−4×35,1−4×0,0−4×1)=(14,1,−4). Tour 2 : q=2 puisque 35=2×14+7. Les triplets deviennent (14,1,−4) et (35−2×14,0−2×1,1−2×(−4))=(7,−2,9). Tour 3 : q=2 puisque 14=2×7+0. Les triplets deviennent (7,−2,9) et (0,5,−22). Le reste du second triplet est nul : l'algorithme s'arrête et renvoie (7,−2,9). Vérification de l'égalité de Bézout : 154×(−2)+35×9=−308+315=7. Et 7 est bien un diviseur commun, puisque 154=7×22 et 35=7×5 : on a donc pgcd(154,35)=7.
2) Existence. Tout entier de la forme 154u+35v est divisible par 7, puisque 7 divise 154 et 35. Comme 7 divise 21, cette condition nécessaire est remplie, et la solution particulière construite ci-dessous montre qu'elle est suffisante ici. Solution particulière. En multipliant l'égalité de Bézout par 721=3, on obtient 154×(−6)+35×27=21 : le couple (u0,v0)=(−6,27) est solution. Analyse. Soit (u,v)∈Z2 une solution. En retranchant membre à membre les deux égalités, 154(u+6)+35(v−27)=0, donc, après division par 7, 22(u+6)=−5(v−27). Ainsi 5 divise 22(u+6). Or pgcd(5,22)=1, donc, d'après le lemme de Gauss, 5 divise u+6 : il existe k∈Z tel que u=−6+5k. En reportant, 22×5k=−5(v−27), donc v−27=−22k, c'est-à-dire v=27−22k. Synthèse. Réciproquement, pour tout k∈Z, 154(−6+5k)+35(27−22k)=21+(154×5−35×22)k=21+(770−770)k=21 : tous ces couples sont solutions. Conclusion. L'ensemble des solutions est {(−6+5k,27−22k):k∈Z}. Pour k=1, on obtient (−1,5), et 154×(−1)+35×5=−154+175=21. Les pas sont 735=5 et 7154=22, et non 35 et 154 : avec ces derniers, le pas serait sept fois trop grand et l'on ne décrirait qu'une solution sur sept.
3) Non. On a vu que pgcd(35,154)=7. Si 35 était inversible modulo 154, il existerait un entier x tel que 35x≡1(mod154), c'est-à-dire un entier y tel que 35x−154y=1 ; alors 7, qui divise 35 et 154, diviserait 1, ce qui est faux. Un entier est inversible modulo n si et seulement s'il est premier avec n, et 35 ne l'est pas avec 154.
4) Existence. L'algorithme d'Euclide donne successivement 154=6×23+16, puis 23=1×16+7, puis 16=2×7+2, puis 7=3×2+1, puis 2=2×1+0. Le dernier reste non nul vaut 1, donc pgcd(23,154)=1 et 23 est inversible modulo 154. Remontée. De la quatrième égalité, 1=7−3×2. En remplaçant 2 par 16−2×7, tiré de la troisième égalité : 1=7−3(16−2×7)=7×7−3×16. En remplaçant 7 par 23−16, tiré de la deuxième : 1=7(23−16)−3×16=7×23−10×16. En remplaçant 16 par 154−6×23, tiré de la première : 1=7×23−10(154−6×23)=67×23−10×154. Réduction. L'égalité 23×67−154×10=1 donne 23×67≡1(mod154), et 67 appartient à {0,…,153} : l'inverse cherché est 67. Vérification : 23×67=1541=154×10+1, donc 1541≡1(mod154). Unicité. Si x et y appartiennent à {0,…,153} et vérifient tous deux 23x≡23y≡1(mod154), alors 154 divise 23(x−y) ; comme pgcd(23,154)=1, le lemme de Gauss donne que 154 divise x−y, et ∣x−y∣≤153<154 force x=y. Contrôle machine, avec une écriture qui applique la même combinaison aux trois composantes :
def euclide_etendu(a, b):
"""Renvoie (d, u, v) tel que d = pgcd(a, b) et a * u + b * v = d.
Precondition : a et b sont des entiers naturels non tous deux nuls."""
triplet, suivant = (a, 1, 0), (b, 0, 1)
while suivant[0] != 0:
q = triplet[0] // suivant[0]
triplet, suivant = suivant, tuple(t - q * s for t, s in zip(triplet, suivant))
return triplet
print(euclide_etendu(154, 35))
print(154 * (-6 + 5) + 35 * (27 - 22))
print(euclide_etendu(23, 154))
print(23 * 67 % 154)
(7, -2, 9)
21
(1, 67, -10)
1
On note que le triplet renvoyé pour 23 et 154 contient directement le couple de Bézout (67,−10), et que le coefficient 67 est déjà réduit ; dans le cas général il ne l'est pas, et l'on écrit u % n.
Exercice 4Classique
Exponentiation rapide : deux versions, deux comptages
1) Écrire une fonction récursive puissance_rec(x, n) qui renvoie x ** n pour n entier naturel, en n'effectuant qu'un seul appel récursif par exécution. 2) Donner la suite des exposants rencontrés pour n=100, puis le nombre total d'appels à la fonction, l'appel initial compris. 3) Écrire la version itérative puissance_rapide(x, n) et énoncer son invariant de boucle. On ne demande pas la démonstration. 4) Donner le nombre exact de multiplications effectuées par la version itérative pour n=100, et retrouver ce nombre sur l'écriture binaire de 100. 5) Écrire puissance_modulaire(x, n, m) qui renvoie x ** n % m sans jamais former la puissance entière, et donner la valeur de puissance_modulaire(7, 10 ** 6, 997). Expliquer enfin, chiffres à l'appui, pourquoi puissance_rapide(7, 10 ** 6) % 997 est inacceptable, bien que la boucle y effectue exactement le même nombre de multiplications.
Voir le corrigé
1) Le cas de base est l'exposant nul, et l'appel récursif porte sur la moitié entière de l'exposant.
def puissance_rec(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
if n == 0:
return 1
demi = puissance_rec(x, n // 2)
if n % 2 == 0:
return demi * demi
return demi * demi * x
La variable demi n'est pas un confort d'écriture : elle est ce qui garantit qu'un appel n'en engendre qu'un seul. Écrire puissance_rec(x, n // 2) * puissance_rec(x, n // 2) donnerait la même valeur et un nombre d'appels qui double à chaque niveau.
2) Chaque appel remplace l'exposant par sa moitié entière, donc la suite des exposants est 100, 50, 25, 12, 6, 3, 1, 0, le dernier déclenchant le cas de base. Elle compte huit termes. Comme un appel engendre exactement un appel tant que son exposant est non nul, il y a autant d'appels que de termes : 8 appels au total, dont 7 récursifs, et la pile atteint une profondeur de 8.
3) On fait tourner trois variables, et la boucle s'arrête quand l'exposant s'annule.
def puissance_rapide(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
resultat = 1
base = x
exposant = n
while exposant > 0:
if exposant % 2 == 1:
resultat = resultat * base
base = base * base
exposant = exposant // 2
return resultat
Invariant : en notant Rk, Bk et Ek les valeurs des trois variables après k tours, on a Rk⋅BkEk=xn. L'invariant porte sur les trois variables à la fois, et non sur resultat seul ; c'est lui qui, évalué en E=0 à la sortie, donne la valeur renvoyée.
4) L'écriture binaire de 100 est 1100100 : sept chiffres, dont trois valent 1. La boucle fait un tour par chiffre binaire, soit sept tours, puisque l'exposant perd un chiffre à chaque division par deux. Chaque tour comporte une élévation au carré, soit sept multiplications, et les tours dont l'exposant courant est impair, au nombre de trois, en ajoutent une chacun. Le total est donc de 7+3=10 multiplications, contre 99 pour la boucle naïve qui multiplierait par x une fois par unité d'exposant.
5) Il suffit de réduire après chaque multiplication, ce qu'autorise la compatibilité du produit avec la congruence : si a≡a′ et b≡b′ modulo m, alors ab≡a′b′ modulo m.
def puissance_modulaire(x, n, m):
"""Renvoie x ** n % m.
Precondition : n est un entier naturel et m >= 1."""
resultat = 1 % m
base = x % m
exposant = n
while exposant > 0:
if exposant % 2 == 1:
resultat = resultat * base % m
base = base * base % m
exposant = exposant // 2
return resultat
print(puissance_rec(3, 5), puissance_rapide(3, 5), 3 ** 5)
print(puissance_modulaire(7, 10 ** 6, 997))
print(puissance_modulaire(2, 0, 7), puissance_modulaire(5, 3, 1))
243 243 243
672
1 0
L'appel demandé vaut donc 672. Pourquoi l'autre écriture est inacceptable. Les deux boucles effectuent le même nombre de multiplications : 106 s'écrit sur vingt chiffres binaires dont sept valent 1, soit 20+7=27 multiplications de part et d'autre. Mais ce décompte porte sur le nombre de multiplications, pas sur leur coût unitaire. Sans réduction, les facteurs grossissent : 7106 compte ⌊106log107⌋+1=845099 chiffres décimaux, et les dernières multiplications portent sur des entiers de cette taille, dont le produit ne se calcule pas en temps constant. Avec la réduction, aucun entier manipulé ne dépasse 9972, soit moins d'un million : les vingt-sept multiplications sont alors des multiplications de mots machine. La dernière ligne du contrôle montre deux cas limites : un exposant nul renvoie le neutre réduit modulo m, et un module égal à 1 renvoie 0, puisque tout entier est congru à 0 modulo 1. C'est pour que ces deux règles ne se contredisent pas en n=0 et m=1 que resultat part de 1 % m et non de 1 : avec 1, la boucle ne tournant pas, la fonction renverrait 1 quand pow(x, 0, 1) vaut 0.
Exercice 5Classique
Le crible, la borne oubliée et la table des plus petits facteurs
1) Écrire crible(n) qui renvoie la liste croissante des nombres premiers inférieurs ou égaux à n, à l'aide d'un tableau de booléens. 2) Dérouler crible(49) : donner, pour chaque valeur prise par la variable de la boucle extérieure, la liste des indices rayés, ou la raison pour laquelle rien n'est rayé. 3) Un élève écrit la boucle intérieure for multiple in range(p * p, n, p). Donner la liste que crible(49) renvoie alors, puis le plus petit entier n pour lequel les deux versions ne renvoient pas la même liste. 4) Modifier le crible pour qu'il renvoie la table ppf, où ppf[k] est le plus petit facteur premier de k pour 2≤k≤n. Expliquer pourquoi le fait de n'écrire que dans une case encore vide suffit à garantir que c'est bien le plus petit facteur qui s'y inscrit, et pourquoi une boucle finale est nécessaire. 5) En déduire une fonction qui factorise un entier k inférieur ou égal à n en un nombre de divisions proportionnel au nombre de facteurs premiers de k comptés avec multiplicité. Donner sa sortie pour k=936, et dire dans quelle situation cette préparation est rentable.
Voir le corrigé
1) On marque les cases, puis on relit le tableau.
def crible(n):
"""Renvoie la liste croissante des nombres premiers inferieurs ou egaux a n.
Precondition : n est un entier naturel."""
premier = [True] * (n + 1)
premier[0] = False
if n >= 1:
premier[1] = False
p = 2
while p * p <= n:
if premier[p]:
for multiple in range(p * p, n + 1, p):
premier[multiple] = False
p = p + 1
return [k for k in range(n + 1) if premier[k]]
Le tableau a n+1 cases pour que l'indice n existe, et le test if n >= 1 protège la case d'indice 1 lorsque n vaut 0.
2) La boucle extérieure tourne tant que p2≤49, donc pour p allant de 2 à 7. Pour p=2 : on raye 4,6,8,…,48. Pour p=3 : on raye 9,12,15,18,21,24,27,30,33,36,39,42,45,48. Pour p=4 : la case d'indice 4 a été rayée au passage de 2, donc rien n'est rayé. C'est normal : tout multiple de 4 est un multiple de 2, déjà traité. Pour p=5 : on raye 25,30,35,40,45. Pour p=6 : la case est déjà rayée, rien n'est rayé. Pour p=7 : on raye 49, seul multiple de 7 supérieur ou égal à 49 et inférieur ou égal à 49. Pour p=8 : la condition 64≤49 est fausse, la boucle s'arrête. Il reste 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47. On voit sur ce déroulé pourquoi le marquage part de p2 : les multiples 2×7, 3×7 et 5×7 ont déjà été rayés lors des passages de 2, de 3 et de 5.
3) Avec range(p * p, n, p), l'indice n n'est jamais atteint, car range s'arrête un cran avant sa borne. L'appel crible(49) renvoie alors [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 49], où 49=7×7 n'est évidemment pas premier : c'était le seul indice rayé au passage de 7, et il ne l'est plus. Le plus petit entier n qui sépare les deux versions est 4 : pour n≤3, la boucle extérieure ne tourne pas ou ne raye rien, et les deux fonctions renvoient la même liste, alors que pour n=4 la version fautive renvoie [2, 3, 4] au lieu de [2, 3]. Plus généralement, les deux versions diffèrent exactement lorsque n est composé, c'est-à-dire lorsque la seule case oubliée aurait dû être rayée.
4) La table se remplit avec le même parcours, mais on n'écrit que dans les cases encore vides.
def table_ppf(n):
"""Renvoie la table ppf ou ppf[k] est le plus petit facteur premier de k,
pour k de 2 a n ; les cases d'indices 0 et 1 valent 0.
Precondition : n est un entier naturel."""
ppf = [0] * (n + 1)
p = 2
while p * p <= n:
if ppf[p] == 0:
for multiple in range(p * p, n + 1, p):
if ppf[multiple] == 0:
ppf[multiple] = p
p = p + 1
for k in range(2, n + 1):
if ppf[k] == 0:
ppf[k] = k
return ppf
Pourquoi la première écriture est la bonne. Les candidats p sont parcourus par valeurs croissantes, donc la première écriture dans la case d'indice k vient du plus petit nombre premier p qui divise k et qui vérifie p2≤k. Or si k est composé et si q désigne son plus petit facteur premier, on a k=qm avec m≥2, et tout facteur premier de m divisant k, il est supérieur ou égal à q : donc m≥q et k≥q2, et la case k appartient bien à la liste des multiples parcourue lors du passage de q. Un k premier, lui, n'appartient à aucune de ces listes, ce qui est précisément la raison de la boucle finale. Comme aucun nombre premier plus petit que q ne divise k, aucune écriture n'a pu avoir lieu avant, et c'est bien q qui s'inscrit. Au passage, le test if ppf[p] == 0 reconnaît exactement les nombres premiers : un p composé a un plus petit facteur premier q vérifiant p≥q2, donc sa case a déjà été écrite. Pourquoi la boucle finale est nécessaire. Un entier composé k est toujours marqué, puisque son plus petit facteur premier q vérifie q2≤k≤n, donc la boucle extérieure atteint bien q. En revanche, un nombre premier n'est marqué par personne : sa case vaut encore 0 à la fin, et la boucle finale y inscrit l'entier lui-même, qui est bien son plus petit facteur premier.
5) On remplace la recherche d'un facteur par une lecture de case.
def factoriser_avec_table(k, ppf):
"""Renvoie la liste des couples (p, alpha) de la decomposition de k.
Precondition : k est un entier entre 2 et la borne de la table ppf."""
facteurs = []
reste = k
while reste > 1:
p = ppf[reste]
alpha = 0
while reste % p == 0:
reste = reste // p
alpha = alpha + 1
facteurs.append((p, alpha))
return facteurs
print(crible(49))
print(table_ppf(20))
print(factoriser_avec_table(936, table_ppf(1000)))
Comme 936=23×32×13, la sortie attendue est bien [(2, 3), (3, 2), (13, 1)]. Chaque tour de la boucle intérieure retire un facteur premier de reste, donc le nombre total de divisions est celui des facteurs premiers de k comptés avec multiplicité, augmenté d'une division par facteur distinct pour constater l'arrêt. Le premier décompte est majoré par log2k, puisque chaque facteur vaut au moins 2 ; le second, qui ne compte que les facteurs distincts, lui est inférieur. Le total est donc majoré par 2log2k, et le coût est O(logk). Pour k=936, cela fait 6+3=9 divisions, quand cette majoration en annonce au plus 19. Quand cette préparation est rentable. Le crible coûte Θ(n) cases de mémoire et O(nloglogn) marquages, payés une seule fois ; chaque factorisation coûte ensuite O(logk) au lieu des O(k) divisions du test par candidats. Dès que l'on doit factoriser beaucoup d'entiers d'un même intervalle, le crible l'emporte largement. Pour une seule question portant sur un entier isolé et très grand, il est au contraire inutilisable, car la mémoire demandée est de l'ordre de la valeur de l'entier.
Exercice 6Rédaction
Euclide par soustractions : terminaison, correction, et le prix payé
On considère la fonction suivante, qui calcule un PGCD sans jamais employer l'opérateur %.
def pgcd_soustractif(a, b):
"""Renvoie le PGCD des entiers naturels a et b.
Precondition : a >= 0, b >= 0, et a et b ne sont pas tous deux nuls."""
while a != 0 and b != 0:
if a >= b:
a = a - b
else:
b = b - a
return a + b
1) Démontrer que pour tous entiers naturels a et b non tous deux nuls avec a≥b, on a pgcd(a−b,b)=pgcd(a,b). 2) Démontrer que la boucle termine, en exhibant un variant. 3) Démontrer que la fonction renvoie pgcd(a,b) : énoncer un invariant, l'initialiser, prouver l'hérédité, puis conclure par la sortie de boucle. 4) Démontrer que pour a=n et b=1, avec n∈N∗, la boucle effectue exactement n tours. Combien de tours pour n=109, et combien de divisions la version avec reste demande-t-elle sur le même couple ?
Voir le corrigé
1) Soient a et b deux entiers naturels non tous deux nuls, avec a≥b. Posons c=a−b, qui est un entier naturel puisque a≥b. Remarquons d'abord que c et b ne sont pas tous deux nuls : s'ils l'étaient, on aurait b=0 puis a=c+b=0, ce qui est exclu. Soit d un diviseur commun de a et b. Il existe des entiers α et β tels que a=dα et b=dβ, donc c=a−b=d(α−β) : l'entier d divise c, donc d est un diviseur commun de c et b. Réciproquement, soit d un diviseur commun de c et b. Alors d divise c+b=a, donc d est un diviseur commun de a et b. Les couples (a,b) et (c,b) ont donc exactement le même ensemble de diviseurs communs. Cet ensemble est non vide, puisqu'il contient 1, et majoré, puisque les deux couples contiennent un entier non nul : il admet donc un plus grand élément, et ce plus grand élément est le même des deux côtés. Autrement dit pgcd(a−b,b)=pgcd(a,b).
2) Terminaison. Notons (ak,bk) les valeurs des deux variables après k tours de boucle, (a0,b0) désignant les valeurs initiales, et posons Vk=ak+bk. Montrons d'abord que ak et bk sont des entiers naturels pour tout k. C'est vrai au rang 0 par la précondition. Si c'est vrai au rang k et que le tour k+1 a lieu, la variable modifiée est remplacée par la différence de la plus grande par la plus petite, qui est un entier naturel, et l'autre ne bouge pas. Supposons maintenant que le tour k+1 ait lieu : c'est que ak=0 et bk=0, donc ak≥1 et bk≥1. Si ak≥bk, alors Vk+1=(ak−bk)+bk=Vk−bk≤Vk−1 ; sinon Vk+1=ak+(bk−ak)=Vk−ak≤Vk−1. La suite (Vk) est donc une suite d'entiers naturels strictement décroissante tant que la boucle tourne. Une telle suite ne peut prendre qu'un nombre fini de valeurs, donc la boucle s'arrête : Vk est un variant.
3) Correction. Considérons, pour chaque tour effectué k, la propriété Ik : « pgcd(ak,bk)=pgcd(a0,b0), et (ak,bk) n'est pas le couple nul ». Initialisation. La première égalité est une identité au rang 0, et la seconde propriété est exactement la précondition : I0 est vraie. Hérédité. Supposons Ik vraie et le tour k+1 effectué. Alors ak=0 et bk=0. Premier cas : ak≥bk. Les affectations donnent (ak+1,bk+1)=(ak−bk,bk). La question 1, appliquée au couple (ak,bk), qui vérifie bien ses hypothèses, donne pgcd(ak+1,bk+1)=pgcd(ak,bk), qui vaut pgcd(a0,b0) par hypothèse de récurrence. De plus bk+1=bk=0, donc le couple n'est pas nul. Second cas : ak<bk. Les affectations donnent (ak+1,bk+1)=(ak,bk−ak). La question 1, appliquée cette fois au couple (bk,ak), donne pgcd(bk−ak,ak)=pgcd(bk,ak), et le PGCD ne dépend pas de l'ordre des deux arguments : on conclut comme précédemment. De plus ak+1=ak=0. Dans les deux cas Ik+1 est vraie, donc, par récurrence, Ik est vraie pour tout tour effectué. Sortie de boucle. Notons K le nombre de tours, fini d'après la question 2. La condition d'arrêt donne aK=0 ou bK=0, et IK interdit qu'ils soient nuls tous les deux. Si bK=0, la valeur renvoyée est aK+0=aK, et pgcd(aK,0)=aK pour tout entier naturel aK. Si aK=0, la valeur renvoyée est bK, et pgcd(0,bK)=bK. Dans les deux cas, la valeur renvoyée est pgcd(aK,bK), qui vaut pgcd(a0,b0) d'après IK : la fonction renvoie bien le PGCD de ses deux arguments. Contrôle :
def pgcd_soustractif(a, b):
"""Renvoie le PGCD des entiers naturels a et b.
Precondition : a >= 0, b >= 0, et a et b ne sont pas tous deux nuls."""
while a != 0 and b != 0:
if a >= b:
a = a - b
else:
b = b - a
return a + b
print(pgcd_soustractif(48, 18), pgcd_soustractif(7, 7))
print(pgcd_soustractif(0, 5), pgcd_soustractif(1071, 462))
6 7
5 21
4) Prenons a0=n avec n≥1, et b0=1. Montrons par récurrence sur k∈{0,…,n} la propriété Hk : « la boucle effectue au moins k tours, et après le k-ième tour le couple vaut (n−k,1) ». Initialisation. Pour k=0, le couple vaut (n,1) : H0 est vraie. Hérédité. Soit k<n tel que Hk soit vraie. Alors ak=n−k≥1 et bk=1 sont tous deux non nuls, donc la condition de boucle est vraie et un tour de plus a lieu. Comme ak≥1=bk, ce tour retranche 1 à a, et le couple devient (n−k−1,1) : Hk+1 est vraie. Au rang k=n, le couple vaut (0,1) : la condition a != 0 est fausse et la boucle s'arrête. Elle a donc effectué exactement n tours, et la fonction renvoie 0+1=1, qui est bien pgcd(n,1). Pour n=109, la boucle effectue donc un milliard de tours. La version qui utilise le reste, elle, calcule 109mod1=0 au premier tour et s'arrête aussitôt : une seule division. Le nombre de tours de la version soustractive n'est donc pas majoré par O(logn) : il est linéaire en la valeur des données, donc exponentiel en leur nombre de chiffres. Le programme reste correct, il n'est qu'inutilisable, ce qui est une panne plus difficile à détecter qu'un résultat faux. À retenir : une soustraction répétée est une division euclidienne faite à la main, et c'est le passage au reste qui fait tomber le coût de linéaire à logarithmique.
Exercice 7Trouver l'erreur
Trouver l'erreur : une exponentiation rapide qui ne l'est pas
À la question « écrire une exponentiation rapide récursive, puis majorer le nombre de multiplications qu'elle effectue », une copie rend ce qui suit. Programme.
def puissance(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
if n == 0:
return 1
if n % 2 == 0:
return puissance(x, n // 2) * puissance(x, n // 2)
return puissance(x, n // 2) * puissance(x, n // 2) * x
Coût, recopié tel qu'il est rédigé sur la copie. « À chaque appel, l'exposant est remplacé par sa moitié entière. La suite des exposants rencontrés est donc n, ⌊n/2⌋, ⌊n/4⌋, et ainsi de suite jusqu'à 0 : elle compte ⌊log2n⌋+2 termes. Chaque appel effectue au plus deux multiplications, donc la fonction effectue au plus 2(⌊log2n⌋+2) multiplications, et son coût est O(logn). » 1) La fonction renvoie-t-elle la bonne valeur ? Justifier. 2) Le raisonnement sur le coût contient une erreur. La nommer, dire à quelle ligne il cesse d'être valable, et dire ce qu'il aurait fallu démontrer. 3) Notons M(n) le nombre de multiplications effectuées par l'appel puissance(x, n). Écrire la relation de récurrence vérifiée par M, puis démontrer que M(n)≥n pour tout n≥1. 4) Donner la valeur exacte de M(2k), puis celle de M(1024), et la comparer à la majoration annoncée sur la copie. 5) Corriger le programme en ne changeant que la façon dont l'appel récursif est utilisé, et rédiger la majoration correcte.
Voir le corrigé
1) Oui, la fonction renvoie bien xn : l'erreur de la copie est entièrement dans le coût. Montrons-le par récurrence forte sur n. Pour n=0, la fonction renvoie 1=x0. Soit n≥1 ; supposons le résultat acquis pour tout exposant strictement inférieur à n. Comme n≥1, on a ⌊n/2⌋<n, donc les deux appels récursifs renvoient tous deux x⌊n/2⌋. Si n est pair, la fonction renvoie (x⌊n/2⌋)2=x2⌊n/2⌋=xn ; si n est impair, elle renvoie en plus un facteur x, soit x2⌊n/2⌋+1=xn. La terminaison suit le même argument : le second argument est un entier naturel qui décroît strictement à chaque appel.
2) L'erreur. Le raisonnement compte les niveaux de l'arbre des appels et les prend pour les appels eux-mêmes. La phrase « la suite des exposants rencontrés est n, ⌊n/2⌋, ⌊n/4⌋… » décrit une seule branche de cet arbre, et elle est exacte comme telle ; l'erreur n'est pas là. La ligne qui cesse d'être valable est la suivante : « chaque appel effectue au plus deux multiplications, donc la fonction effectue au plus 2(⌊log2n⌋+2) multiplications ». Multiplier le coût d'un appel par le nombre de termes de la suite des exposants suppose qu'il y a exactement un appel par terme. Or le corps de la fonction contient deux appels récursifs : le nombre d'appels d'un niveau double d'un niveau au suivant, et le nombre total d'appels n'est pas le nombre de niveaux. Ce qu'il aurait fallu démontrer : une majoration du nombre total d'appels, ou une relation de récurrence portant sur le nombre de multiplications, puis sa résolution. C'est ce que fait la question suivante, et le résultat contredit la conclusion de la copie.
3) Un appel d'exposant nul n'effectue aucune multiplication. Un appel d'exposant n≥1 pair effectue les multiplications de ses deux appels récursifs, plus une ; s'il est impair, plus deux. Donc M(0)=0 et, pour tout n≥1, M(n)=2M(⌊2n⌋)+1 si n est pair, et M(n)=2M(⌊2n⌋)+2 si n est impair. Dans les deux cas, M(n)≥2M(⌊2n⌋)+1, et c'est cette minoration qui suffit. Montrons par récurrence forte sur n≥1 la propriété P(n) : « M(n)≥n ». Initialisation. Pour n=1, l'exposant est impair et les deux appels récursifs portent sur 0, donc M(1)=2M(0)+2=2≥1 : P(1) est vraie. Hérédité. Soit n≥2 ; supposons P(m) vraie pour tout entier m tel que 1≤m<n. Comme n≥2, on a ⌊2n⌋≥1 et ⌊2n⌋<n, donc l'hypothèse de récurrence forte s'applique à cet entier : M(⌊2n⌋)≥⌊2n⌋. Alors M(n)≥2⌊2n⌋+1≥(n−1)+1=n, la dernière inégalité venant de 2⌊2n⌋≥n−1, vraie que n soit pair ou impair. Par récurrence forte, M(n)≥n pour tout n≥1. Le nombre de multiplications est donc au moins linéaire en n. Il n'est pas O(logn) : s'il l'était, le quotient log2nn serait majoré, alors qu'il tend vers l'infini.
4) Pour n=2k avec k≥1, l'exposant est pair et sa moitié est 2k−1, donc M(2k)=2M(2k−1)+1. En ajoutant 1 de part et d'autre, M(2k)+1=2(M(2k−1)+1) : la suite (M(2k)+1)k≥0 est géométrique de raison 2, de premier terme M(1)+1=3. Donc M(2k)+1=3×2k, c'est-à-dire M(2k)=3×2k−1. Pour n=1024=210, on obtient M(1024)=3×1024−1=3071 multiplications, quand la copie en annonçait au plus 2(10+2)=24. L'écart n'est pas une question de constante : la copie annonce une quantité logarithmique là où la réalité est linéaire.
5) Il suffit de nommer le résultat de l'appel récursif pour ne le calculer qu'une fois.
def puissance(x, n):
"""Renvoie x ** n.
Precondition : n est un entier naturel."""
if n == 0:
return 1
demi = puissance(x, n // 2)
if n % 2 == 0:
return demi * demi
return demi * demi * x
Le nombre de multiplications vérifie alors M(0)=0 et M(n)≤M(⌊2n⌋)+2 pour n≥1. Montrons par récurrence forte que M(n)≤2(⌊log2n⌋+1) pour tout n≥1. Pour n=1, M(1)=2 et le membre de droite vaut 2. Soit n≥2, la propriété étant supposée acquise pour tout entier compris entre 1 et n−1 : comme ⌊2n⌋≥1 et que ⌊log2⌊2n⌋⌋=⌊log2n⌋−1, on obtient M(n)≤2⌊log2n⌋+2, qui est la majoration annoncée. Le coût est donc bien O(logn), cette fois démontré. Contrôle des deux comptages :
def multiplications_double(n):
"""Nombre de multiplications de la version de la copie."""
if n == 0:
return 0
if n % 2 == 0:
return 2 * multiplications_double(n // 2) + 1
return 2 * multiplications_double(n // 2) + 2
def multiplications_corrigee(n):
"""Nombre de multiplications de la version corrigee."""
if n == 0:
return 0
if n % 2 == 0:
return multiplications_corrigee(n // 2) + 1
return multiplications_corrigee(n // 2) + 2
print(multiplications_double(1024), multiplications_corrigee(1024))
print(multiplications_double(100), multiplications_corrigee(100))
3071 12
227 10
À retenir : dans une récursion, ce qui se divise par deux est l'argument, pas le travail, et seule la relation de récurrence sur le coût décide du reste. Une ligne qui appelle deux fois la même fonction avec les mêmes arguments est le signe visible d'un coût qui explose.
Exercice 8Approfondissement
Fibonacci au rang un milliard de milliards, modulo mille
On note (Fn) la suite de Fibonacci : F0=0, F1=1 et Fn+2=Fn+1+Fn. Rappel hors ligne, en une phrase : le produit de deux matrices carrées de taille 2 a pour coefficient d'indice (i,j) la somme ∑k=12Ai,kBk,j, il est associatif et il admet la matrice identité pour neutre, mais il n'est pas commutatif. On pose M=(1110), représentée en Python par la liste de listes [[1, 1], [1, 0]]. 1) Démontrer que pour tout n≥1, Mn=(Fn+1FnFnFn−1). 2) Écrire produit_mod(A, B, m) qui renvoie le produit de deux matrices carrées de taille 2 réduit modulo m, puis puissance_mat_mod(A, n, m) qui calcule An modulo m par exponentiation rapide. Dire quelle propriété du produit matriciel autorise à réutiliser l'algorithme écrit pour les entiers, et laquelle n'est pas nécessaire. 3) En déduire fibonacci_mod(n, m). Vérifier sur F10, puis sur F100mod1000, sachant que F100=354224848179261915075. 4) Donner F1018mod1000 et le nombre de produits de matrices que ce calcul demande. 5) Comparer avec la boucle qui calcule Fn de proche en proche : nombre d'opérations, et taille des entiers manipulés si l'on ne réduit pas modulo m.
Voir le corrigé
1) Montrons par récurrence sur n≥1 la propriété P(n) : « Mn=(Fn+1FnFnFn−1) ». Initialisation. Pour n=1, on a F2=1, F1=1 et F0=0, donc la matrice annoncée est (1110), c'est-à-dire M : P(1) est vraie. Hérédité. Soit n≥1 tel que P(n) soit vraie. Alors Mn+1=MnM=(Fn+1FnFnFn−1)(1110)=(Fn+1+FnFn+Fn−1Fn+1Fn). La relation de récurrence de la suite donne Fn+1+Fn=Fn+2 et Fn+Fn−1=Fn+1, donc Mn+1=(Fn+2Fn+1Fn+1Fn), qui est bien la matrice annoncée au rang n+1. Par récurrence, P(n) est vraie pour tout n≥1. On remarque que la formule vaut encore pour n=0 si l'on convient que M0 est l'identité et que F−1=1, ce qui est cohérent avec F1=F0+F−1.
2) Le produit de deux matrices de taille 2 s'écrit coefficient par coefficient, et l'on réduit chacun d'eux aussitôt.
def produit_mod(A, B, m):
"""Renvoie le produit des matrices carrees de taille 2 A et B, modulo m.
Precondition : m >= 1."""
return [
[(A[0][0] * B[0][0] + A[0][1] * B[1][0]) % m,
(A[0][0] * B[0][1] + A[0][1] * B[1][1]) % m],
[(A[1][0] * B[0][0] + A[1][1] * B[1][0]) % m,
(A[1][0] * B[0][1] + A[1][1] * B[1][1]) % m],
]
def puissance_mat_mod(A, n, m):
"""Renvoie A ** n modulo m, coefficient par coefficient.
Precondition : n est un entier naturel et m >= 1."""
resultat = [[1 % m, 0], [0, 1 % m]]
base = [[A[0][0] % m, A[0][1] % m], [A[1][0] % m, A[1][1] % m]]
exposant = n
while exposant > 0:
if exposant % 2 == 1:
resultat = produit_mod(resultat, base, m)
base = produit_mod(base, base, m)
exposant = exposant // 2
return resultat
Ce qui autorise la réutilisation de l'algorithme, c'est l'associativité du produit et l'existence d'un neutre : l'exponentiation rapide ne fait que regrouper les facteurs d'un produit de n copies de la même matrice, et partir de l'identité. La commutativité n'est jamais employée, puisque l'ordre des facteurs n'est jamais modifié ; c'est pourquoi le même algorithme vaut pour les entiers, les matrices et les classes modulo un entier. La réduction en cours de route est licite pour la même raison que sur les entiers : la congruence est compatible avec la somme et avec le produit, donc réduire chaque coefficient après chaque opération ne change pas les coefficients finaux modulo m.
3) Le coefficient d'indice (1,2) de Mn est Fn d'après la question 1, et cette lecture vaut encore pour n=0, puisque l'identité a un coefficient nul à cette place.
def fibonacci_mod(n, m):
"""Renvoie F(n) modulo m, avec F(0) = 0 et F(1) = 1.
Precondition : n est un entier naturel et m >= 1."""
return puissance_mat_mod([[1, 1], [1, 0]], n, m)[0][1]
print(fibonacci_mod(10, 10 ** 9), fibonacci_mod(100, 1000))
print([fibonacci_mod(k, 10 ** 9) for k in range(11)])
print(fibonacci_mod(10 ** 18, 1000))
55 75
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
875
La première ligne donne F10=55, et 75 pour F100mod1000, ce que confirment les trois derniers chiffres de F100=354224848179261915075. La deuxième ligne redonne les onze premiers termes de la suite.
4) La troisième ligne du contrôle donne F1018mod1000=875. L'entier 1018 s'écrit sur 60 chiffres binaires, dont 24 valent 1. La boucle fait donc 60 tours, chacun comportant une élévation au carré, et les 24 tours d'exposant impair ajoutent chacun un produit : le calcul demande 84 produits de matrices, soit 84×8=672 multiplications d'entiers tous strictement inférieurs à 1000. C'est instantané.
5) La boucle de proche en proche effectue n additions. Pour n=1018, même à un milliard d'additions par seconde, il faudrait environ 109 secondes, soit plus de trente ans : le calcul est hors d'atteinte, alors que le nombre d'opérations du calcul matriciel est de l'ordre de log2n, c'est-à-dire d'une soixantaine. Quant à la taille des entiers, si l'on ne réduit pas modulo m, elle croît linéairement avec le rang : Fn est équivalent à 5φn, où φ désigne le nombre d'or, donc Fn compte environ nlog10(φ) chiffres décimaux, soit environ 0,209n. On le vérifie sur les valeurs citées : F100 a bien 21 chiffres, et F1000 en a 209. Sans réduction, F1018 aurait donc environ 2×1017 chiffres décimaux, et aucune machine ne peut l'écrire : c'est la réduction modulo m, et non l'exponentiation rapide seule, qui rend la question calculable.
QCM
12 questions : clique sur une réponse pour voir la correction.
Question 1
En Python, que valent -17 // 5 et -17 % 5 ?
Question 2
Les variables a et b valent respectivement 45 et 12. Que valent-elles après l'exécution de la ligne a, b = b, a % b ?
Question 3
Que produit ce programme ?
a, b = 4004, 1540
n = 0
while b != 0:
a, b = b, a % b
n = n + 1
print(a, n)
Question 4
def nombre_de_diviseurs(n):
compte = 0
for k in range(1, n + 1):
if n % k == 0:
compte = compte + 1
return compte
Combien de divisions l'appel nombre_de_diviseurs(n) effectue-t-il, pour n grand ?
Question 5
La fonction suivante évalue le polynôme P=∑j=0najXj, représenté par la liste de ses coefficients par degrés croissants.
def horner(P, x):
valeur = 0
for i in range(len(P) - 1, -1, -1):
valeur = valeur * x + P[i]
return valeur
On note vi la valeur de valeur juste après le traitement de l'indice i. Quelle égalité est vraie quels que soient le polynôme, le point x et l'indice i ?
Question 6
Que produit ce programme ?
def normaliser_en_place(P):
while P != [] and P[-1] == 0:
P.pop()
return P
A = [5, 0, 0]
B = normaliser_en_place(A)
B.append(7)
print(A)
Question 7
def puissance(x, n):
if n == 0:
return 1
if n % 2 == 0:
return puissance(x, n // 2) * puissance(x, n // 2)
return puissance(x, n // 2) * puissance(x, n // 2) * x
Combien d'appels à puissance l'évaluation de puissance(2, 8) déclenche-t-elle en tout, l'appel initial compris ?
Question 8
La fonction puissance_rapide suit la boucle usuelle : tant que l'exposant est non nul, elle multiplie le résultat par la base si l'exposant est impair, élève la base au carré, puis divise l'exposant par deux. Combien de multiplications effectue-t-elle exactement pour n=200 ?
Question 9
On veut calculer 7109mod13. Laquelle de ces écritures rend le bon résultat avec des multiplications qui portent toutes sur des entiers inférieurs à 132 ?
Question 10
Le crible d'Ératosthène raye les cases d'indices p2, p2+p, p2+2p, et ainsi de suite jusqu'à n, pour chaque p encore marqué premier tant que p2≤n. Lors de l'exécution de crible(50), combien de fois la case d'indice 45 est-elle mise à False ?
Question 11
L'algorithme d'Euclide étendu appliqué à a=18 et b=30 renvoie le triplet (6,2,−1), et l'on vérifie 18×2+30×(−1)=6. Quel autre couple (u,v) d'entiers vérifie 18u+30v=6 ?
Question 12
Les polynômes sont représentés par la liste de leurs coefficients par degrés croissants, et toute fonction renvoie une liste normalisée, c'est-à-dire vide ou de dernier coefficient non nul. Que renvoie produit_poly([1, 2, 0], [0, 3]) ?