Les exercices sont classes par difficulte croissante. Pour chacun, chercher au brouillon avant d'ouvrir le corrige : c'est la seule facon de progresser. Les enonces marques Concours sont d'un niveau CNC / e3a.
Donner, sans machine, la valeur et le type de chacune des expressions suivantes, puis verifier.
a) 7 / 2 b) 7 // 2 c) -7 // 2 d) -7 % 2
e) 2 ** 3 ** 2 f) -3 ** 2 g) int(-3.7) h) round(-3.5)
i) True + True j) bool("False") k) 10 % -3 l) 5 == 5.0Afficher le corrige
| Valeur | Type | Justification | |
|---|---|---|---|
| a | 3.5 | float | / renvoie toujours un flottant |
| b | 3 | int | quotient euclidien |
| c | -4 | int | arrondi vers \(-\infty\) : \(-7 = 2\times(-4)+1\) |
| d | 1 | int | le reste a le signe du diviseur (positif) |
| e | 512 | int | ** associatif a droite : \(2^{9}\) |
| f | -9 | int | ** prioritaire sur le moins unaire |
| g | -3 | int | troncature vers 0, pas d'arrondi |
| h | -4 | int | arrondi au pair le plus proche (banker's rounding) |
| i | 2 | int | bool est un sous-type de int |
| j | True | bool | chaine non vide donc vraie |
| k | -2 | int | \(10 = (-3)\times(-4) + (-2)\), reste du signe de \(-3\) |
| l | True | bool | comparaison numerique, les types different mais les valeurs non |
round applique l'arrondi au pair le plus proche en cas d'egalite :
round(0.5) vaut 0, round(1.5) vaut 2, round(2.5) vaut 2.
C'est la convention IEEE 754, qui evite un biais statistique.
Soit n un entier a exactement 3 chiffres (100 ≤ n ≤ 999).
- Ecrire des expressions donnant le chiffre des unites, des dizaines, des centaines.
- En deduire une fonction
somme_chiffres3(n). - Ecrire
renverse3(n)qui renvoie l'entier obtenu en lisantna l'envers (renverse3(123)vaut 321).
Afficher le corrige
def somme_chiffres3(n):
"""Somme des chiffres d'un entier a 3 chiffres."""
u = n % 10
d = (n // 10) % 10
c = n // 100
return u + d + c
def renverse3(n):
u = n % 10
d = (n // 10) % 10
c = n // 100
return 100*u + 10*d + c
assert somme_chiffres3(123) == 6
assert renverse3(123) == 321
assert renverse3(100) == 1 # 001 = 1
n % 10 donne le dernier chiffre, n // 10 supprime le dernier
chiffre. Ce couple d'operations est la base de tous les algorithmes de manipulation de chiffres
(somme des chiffres, conversion de base, test de palindrome numerique).
Ecrire un programme qui demande une duree en secondes (entier positif) et l'affiche sous la
forme h:mm:ss, par exemple 2:05:09 pour 7509 secondes.
Afficher le corrige
total = int(input("Duree en secondes : "))
heures = total // 3600
reste = total % 3600
minutes = reste // 60
secondes = reste % 60
print(f"{heures}:{minutes:02d}:{secondes:02d}")
Le format :02d impose deux chiffres en completant par un zero a gauche. On peut
aussi obtenir les deux divisions d'un coup :
minutes, secondes = divmod(total, 60)
heures, minutes = divmod(minutes, 60)
divmod(a, b) renvoie le couple (a // b, a % b).
- Expliquer pourquoi
0.1 + 0.2 == 0.3renvoieFalse. - Ecrire
presque_egal(a, b, eps=1e-9)testant l'egalite aepspres en valeur relative. - Pourquoi une tolerance relative est-elle preferable a une tolerance absolue ?
- Que renvoie votre fonction si
a = b = 0.0? Corriger si necessaire.
Afficher le corrige
1. En base 2, \(0{,}1 = 1/10\) n'a pas d'ecriture finie (10 n'est pas une
puissance de 2), exactement comme \(1/3\) en base 10. Le flottant stocke est donc le plus proche
representable sur 53 bits de mantisse, soit une erreur de l'ordre de \(10^{-17}\). En sommant
deux tels arrondis on obtient 0.30000000000000004, distinct du flottant le plus
proche de 0,3.
def presque_egal(a, b, eps=1e-9):
"""Test d'egalite relative de deux flottants."""
m = max(abs(a), abs(b))
if m == 0.0: # a et b sont tous deux nuls
return True
return abs(a - b) <= eps * m
assert presque_egal(0.1 + 0.2, 0.3)
assert presque_egal(0.0, 0.0)
assert not presque_egal(1.0, 1.1)
3. Une tolerance absolue abs(a-b) < 1e-9 est trop severe pour
de grands nombres (deux flottants voisins autour de \(10^{12}\) different deja de \(10^{-4}\) :
le test echouerait toujours) et trop laxiste pour de tres petits (autour de \(10^{-15}\), tous
les nombres seraient declares egaux). La tolerance relative mesure l'erreur en nombre de
chiffres significatifs, ce qui est la bonne notion pour des flottants.
4. Sans le test m == 0, on obtiendrait 0 <= 0,
soit True : le resultat est heureusement correct ici, mais le cas merite d'etre
explicite. En pratique, la bibliotheque standard fournit
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0) qui combine les deux tolerances.
- Que produisent ces deux lignes ? Justifier.
n = 0 print(n != 0 and 10 / n > 1) print(10 / n > 1 and n != 0) # ZeroDivisionError attendue : pourquoi ? - Soit
tune liste. Expliquer pourquoilen(t) > 0 and t[0] == 5est sur alors que l'ordre inverse ne l'est pas. - Que vaut
0 or "" or [] or "dernier"? Et3 and 0 and 7?
Afficher le corrige
1. La premiere ligne affiche False : n != 0 est
faux, donc and court-circuite et 10 / n n'est jamais evalue.
La seconde leve ZeroDivisionError : l'operande gauche est evalue en premier. Les
connecteurs logiques ne sont donc pas commutatifs a l'execution, meme s'ils le sont
logiquement.
2. Si t est vide, len(t) > 0 est faux et
t[0] n'est pas evalue. Dans l'ordre inverse, t[0] leverait
IndexError. C'est le motif standard de la garde : placer a gauche la
condition qui rend la droite licite.
3. or renvoie le premier operande vrai, ou le dernier si tous
sont faux : ici "dernier". and renvoie le premier operande faux, ou le
dernier si tous sont vrais : ici 0. Attention, ces operateurs ne renvoient pas
necessairement un booleen mais l'un des operandes.
On veut resoudre \(ax^2+bx+c=0\) avec \(a \neq 0\).
- Ecrire
racines(a, b, c)par la formule usuelle. - Tester avec \(a=1\), \(b=10^{8}\), \(c=1\). Comparer la racine calculee la plus proche de zero a la valeur theorique \(\approx -10^{-8}\). Que constate-t-on ?
- Expliquer le phenomene, puis proposer une formule numeriquement stable.
Afficher le corrige
1-2. La formule usuelle donne pour la racine
\(r_+ = \frac{-b+\sqrt{\Delta}}{2a}\) le calcul (-1e8 + sqrt(1e16 - 4)) / 2. Or
\(\sqrt{10^{16}-4} \approx 10^{8} - 2\cdot 10^{-8}\) : on soustrait deux nombres presque egaux,
tous deux d'ordre \(10^8\). Les 8 premiers chiffres significatifs s'annulent, il ne reste que
du bruit d'arrondi. Python renvoie 0.0 au lieu de \(-10^{-8}\) : toute
precision est perdue. C'est le phenomene de cancellation catastrophique.
3. L'autre racine, elle, est calculee sans soustraction et donc bien. On calcule celle-ci en premier, puis on obtient la seconde par la relation produit \(r_1 r_2 = c/a\). On pose \(q = -\tfrac12\bigl(b + \operatorname{sgn}(b)\sqrt{\Delta}\bigr)\), de sorte que l'addition se fasse toujours entre nombres de meme signe :
from math import sqrt
def racines_stables(a, b, c):
"""Racines reelles de a x^2 + b x + c, sans cancellation."""
d = b*b - 4*a*c
if d < 0:
return ()
if d == 0:
return (-b / (2*a),)
r = sqrt(d)
signe = 1.0 if b >= 0 else -1.0
q = -0.5 * (b + signe * r) # addition de deux nombres de meme signe
r1, r2 = q / a, c / q # produit des racines = c/a
return (min(r1, r2), max(r1, r2))
print(racines_stables(1, 1e8, 1))
# (-1e+08, -1.0000000000000001e-08) : les deux racines sont correctes
Ne jamais soustraire deux quantites voisines lorsqu'on peut l'eviter. On reecrit l'expression en utilisant une identite algebrique (ici la quantite conjuguee : \(\frac{-b+\sqrt\Delta}{2a} = \frac{2c}{-b-\sqrt\Delta}\)). Ce reflexe est reutilise au chapitre 20.
On souhaite calculer \(H_n = \sum_{k=1}^{n} \frac{1}{k}\) pour \(n = 10^7\).
- Ecrire deux fonctions sommant respectivement dans l'ordre croissant puis decroissant des indices.
- Les valeurs obtenues coincident-elles ? Laquelle est la plus precise ?
- Justifier theoriquement.
Afficher le corrige
def h_croissant(n):
s = 0.0
for k in range(1, n+1):
s += 1/k
return s
def h_decroissant(n):
s = 0.0
for k in range(n, 0, -1):
s += 1/k
return s
n = 10**7
print(h_croissant(n)) # 16.695311365857272
print(h_decroissant(n)) # 16.695311365859965
Les deux resultats different a partir du 12e chiffre significatif. La sommation decroissante est plus precise.
Justification. A chaque addition s += x, le resultat est arrondi
au flottant le plus proche : l'erreur commise est de l'ordre de \(\varepsilon |s|\) ou
\(\varepsilon \approx 2{,}2\cdot 10^{-16}\) est l'epsilon machine. Dans l'ordre croissant,
l'accumulateur \(s\) atteint vite une valeur grande devant les termes ajoutes : lorsque
\(1/k \ll \varepsilon\, s\), le terme est purement et simplement absorbe et n'apporte
rien. En sommant du plus petit au plus grand, les petits termes se cumulent d'abord entre eux et
forment une contribution qui, elle, n'est plus negligeable.
L'algorithme de sommation compensee de Kahan conserve dans une variable auxiliaire la partie perdue a chaque arrondi et la reinjecte, ramenant l'erreur totale de \(O(n\varepsilon)\) a \(O(\varepsilon)\) :
def kahan(n):
s, c = 0.0, 0.0
for k in range(1, n+1):
y = 1/k - c
t = s + y
c = (t - s) - y # ce qui a ete perdu dans l'addition
s = t
return sSoit \(b \ge 2\) un entier. Tout entier \(n \ge 0\) s'ecrit de maniere unique \(n = \sum_{i=0}^{p} c_i b^i\) avec \(0 \le c_i < b\).
- Ecrire
chiffres(n, b)renvoyant la liste \([c_0, c_1, \dots, c_p]\) (poids faibles en tete), sans utiliserbinnihex. - Ecrire la fonction reciproque
valeur(c, b), en \(O(p)\) multiplications (schema de Horner). - Montrer que le nombre de tours de boucle de
chiffresest \(\lfloor \log_b n \rfloor + 1\) ; en deduire sa complexite. - Prouver la terminaison de la boucle de
chiffresa l'aide d'un variant.
Afficher le corrige
def chiffres(n, b):
"""Chiffres de n en base b, poids faibles en tete. n >= 0, b >= 2."""
if n == 0:
return [0]
c = []
while n > 0:
c.append(n % b)
n = n // b
return c
def valeur(c, b):
"""Reciproque de chiffres : evaluation par schema de Horner."""
v = 0
for x in reversed(c): # des poids forts vers les poids faibles
v = v * b + x
return v
assert chiffres(13, 2) == [1, 0, 1, 1] # 13 = 1101_2
assert valeur([1, 0, 1, 1], 2) == 13
assert chiffres(255, 16) == [15, 15]
for n in range(500):
assert valeur(chiffres(n, 7), 7) == n
3. Nombre d'iterations. Notons \(n_0 = n\) et \(n_{j+1} = \lfloor n_j/b
\rfloor\). Une recurrence immediate donne \(n_j = \lfloor n/b^j \rfloor\). La boucle s'arrete au
premier \(j\) tel que \(n_j = 0\), c'est-a-dire au premier \(j\) tel que \(n < b^j\), soit
\(j = \lfloor \log_b n\rfloor + 1\). Chaque tour effectue une division euclidienne et un
append en temps constant (sur des entiers machine), d'ou une complexite en
\(\Theta(\log_b n)\).
4. Terminaison. On prend pour variant la quantite entiere \(v_j = n_j\), qui est un entier naturel a chaque tour puisque \(n \ge 0\) et que la division entiere d'un entier positif reste positive. Elle est strictement decroissante : tant que la condition \(n_j > 0\) est vraie et que \(b \ge 2\), on a \(n_{j+1} = \lfloor n_j/b \rfloor \le n_j/2 < n_j\). Une suite d'entiers naturels strictement decroissante etant finie, la boucle effectue un nombre fini d'iterations : elle termine.
Evaluer \(\sum c_i b^i\) naivement coute \(O(p^2)\) multiplications si l'on recalcule \(b^i\) a chaque fois. Le schema de Horner \(((c_p b + c_{p-1})b + c_{p-2})b + \cdots\) n'en utilise que \(p\). Ce meme schema sert a evaluer un polynome ; on le retrouvera au chapitre 10.