Partie I - Fondamentaux Python · Chapitre 1 · Fiche d'exercices

Notions de base

8 exercices corriges · difficulte croissante

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.

Exercice 1Types et valeurs : predire avant d'executerApplication

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.0
Afficher le corrige
ValeurTypeJustification
a3.5float/ renvoie toujours un flottant
b3intquotient euclidien
c-4intarrondi vers \(-\infty\) : \(-7 = 2\times(-4)+1\)
d1intle reste a le signe du diviseur (positif)
e512int** associatif a droite : \(2^{9}\)
f-9int** prioritaire sur le moins unaire
g-3inttroncature vers 0, pas d'arrondi
h-4intarrondi au pair le plus proche (banker's rounding)
i2intbool est un sous-type de int
jTrueboolchaine non vide donc vraie
k-2int\(10 = (-3)\times(-4) + (-2)\), reste du signe de \(-3\)
lTrueboolcomparaison numerique, les types different mais les valeurs non
A noter sur (h)

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.

Exercice 2Extraction de chiffresApplication

Soit n un entier a exactement 3 chiffres (100 ≤ n ≤ 999).

  1. Ecrire des expressions donnant le chiffre des unites, des dizaines, des centaines.
  2. En deduire une fonction somme_chiffres3(n).
  3. Ecrire renverse3(n) qui renvoie l'entier obtenu en lisant n a 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
A retenir

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).

Exercice 3Conversions d'unites et formatageApplication

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).

Exercice 4Le piege des flottantsEntrainement
  1. Expliquer pourquoi 0.1 + 0.2 == 0.3 renvoie False.
  2. Ecrire presque_egal(a, b, eps=1e-9) testant l'egalite a eps pres en valeur relative.
  3. Pourquoi une tolerance relative est-elle preferable a une tolerance absolue ?
  4. 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.

Exercice 5Ordre d'evaluation et court-circuitEntrainement
  1. 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 ?
  2. Soit t une liste. Expliquer pourquoi len(t) > 0 and t[0] == 5 est sur alors que l'ordre inverse ne l'est pas.
  3. Que vaut 0 or "" or [] or "dernier" ? Et 3 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.

Exercice 6Racines d'un trinome : robustesse numeriqueApprofondissement

On veut resoudre \(ax^2+bx+c=0\) avec \(a \neq 0\).

  1. Ecrire racines(a, b, c) par la formule usuelle.
  2. 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 ?
  3. 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
Regle generale

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.

Exercice 7Somme harmonique et ordre de sommationApprofondissement

On souhaite calculer \(H_n = \sum_{k=1}^{n} \frac{1}{k}\) pour \(n = 10^7\).

  1. Ecrire deux fonctions sommant respectivement dans l'ordre croissant puis decroissant des indices.
  2. Les valeurs obtenues coincident-elles ? Laquelle est la plus precise ?
  3. 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.

Pour aller plus loin

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 s
Exercice 8Chiffres d'un entier en base quelconqueConcours

Soit \(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\).

  1. Ecrire chiffres(n, b) renvoyant la liste \([c_0, c_1, \dots, c_p]\) (poids faibles en tete), sans utiliser bin ni hex.
  2. Ecrire la fonction reciproque valeur(c, b), en \(O(p)\) multiplications (schema de Horner).
  3. Montrer que le nombre de tours de boucle de chiffres est \(\lfloor \log_b n \rfloor + 1\) ; en deduire sa complexite.
  4. Prouver la terminaison de la boucle de chiffres a 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.

Remarque — Horner

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.

Retour au coursChapitre 1 — Notions de base
Relire le cours →