Vue d'ensemble
Un programme qui « marche sur les exemples » n'est pas un programme prouvé. Aux concours comme en khôlle, on te demande de justifier qu'une fonction fait bien ce qu'elle promet : qu'elle s'arrête (terminaison) et qu'elle renvoie le bon résultat (correction). Deux outils suffisent pour les boucles : l'invariant (une propriété conservée à chaque tour, qui prouve la correction) et le variant (une quantité entière qui décroît, qui prouve la terminaison). Cette fiche pose le vocabulaire, puis déroule une preuve complète du début à la fin.
Prérequis
- Écrire et dérouler une boucle
while(condition, corps, progression) - Le motif de l'accumulateur (une variable mise à jour à chaque tour)
- Manipuler les listes : indexation
t[i], longueurlen(t) - Raisonner par récurrence sur les entiers
Tu sais coder la boucle mais tu sèches quand on te demande de la « prouver » ? C'est le réflexe qui sépare une copie moyenne d'une bonne copie. Nos mentors alumni X · Centrale · Mines t'apprennent à rédiger invariant, variant et conclusion dans le bon ordre, sur tes propres DS, jusqu'à ce que ça coule tout seul.
Trouver un mentor →1. Spécifier une fonction : préconditions et postconditions
La précondition d'une fonction est ce qu'elle suppose vrai sur ses arguments pour fonctionner (à la charge de l'appelant). La postcondition est ce qu'elle garantit sur son résultat lorsque la précondition est respectée. Spécifier une fonction, c'est écrire ce contrat avant d'écrire le code.
def racine_entiere(n):
# Precondition : n est un entier, n >= 0
# Postcondition : renvoie r tel que r*r <= n < (r+1)*(r+1)
r = 0
while (r + 1) * (r + 1) <= n:
r = r + 1
return r# Precondition : n >= 0Ce qu'on exige de l'appelant. La fonction n'a de sens que pour un entier positif ou nul. Sur un n négatif, on ne promet rien : c'est un usage hors contrat.# Postcondition : r*r <= n < (r+1)*(r+1)Ce qu'on promet en retour. Le résultat r est la partie entière de : le plus grand entier dont le carré ne dépasse pas n.while (r + 1) * (r + 1) <= n:On avance tant qu'on peut. On augmente r d'un cran tant que (r+1)² reste . Dès que (r+1)² dépasse n, on s'arrête : r vérifie la postcondition.2. Les assertions : l'instruction assert
L'instruction assert condition vérifie, pendant l'exécution, qu'une
propriété est vraie. Si la condition est vraie, le programme continue sans rien changer ; si elle
est fausse, il s'arrête aussitôt en levant une AssertionError. C'est le moyen le plus
simple de faire respecter une précondition et de tester ses hypothèses.
def racine_entiere(n):
assert n >= 0, "n doit etre un entier positif ou nul"
r = 0
while (r + 1) * (r + 1) <= n:
r = r + 1
return r
print(racine_entiere(17)) # 4
# racine_entiere(-2) leverait AssertionErrorassert n >= 0, "..."On matérialise la précondition dans le code. Si n < 0, l'exécution s'arrête net avec le message donné : on repère l'appel fautif immédiatement, au lieu d'obtenir un résultat faux plus loin.print(racine_entiere(17))Usage conforme. 17 >= 0 est vrai : l'assertion passe silencieusement et la fonction renvoie 4 (car 4^2 = 16 \leqslant 17 < 25 = 5^2).# racine_entiere(-2)Usage hors contrat. Ici l'assertion serait fausse et lèverait AssertionError : le bug est signalé à la source.if, pas un assert.
3. Invariant de boucle : prouver la correction
Un invariant d'une boucle est une propriété P qui est vraie
avant d'entrer dans la boucle et qui reste vraie après chaque tour (on dit
qu'elle est préservée). Bien choisi, l'invariant, combiné à la condition d'arrêt, donne
directement la correction du résultat.
def maximum(t):
# Precondition : t est une liste non vide
m = t[0]
i = 1
while i < len(t):
if t[i] > m:
m = t[i]
i = i + 1
return mm = t[0]Le meilleur candidat de départ. Avant tout parcours, le maximum vu se limite au premier élément.while i < len(t):On parcourt le reste. i désigne la case qu'on s'apprête à examiner ; les cases t[0] à t[i-1] ont déjà été traitées.if t[i] > m:
m = t[i]Mise à jour du record. Si l'élément courant bat le maximum courant, il devient le nouveau maximum.
Au début de chaque tour, m est le maximum de la tranche déjà vue, c'est-à-dire
. Initialisation : avant la boucle,
i = 1 et m = t[0], donc , vrai. Conservation : si
m est le max de t[0..i-1], alors après avoir comparé t[i] et
incrémenté i, m devient le max de t[0..i]. À la sortie
i = len(t), donc m est le max de tout le tableau : le résultat est correct.
- Regarde ce que vaut la variable de résultat après quelques tours : c'est souvent « le résultat partiel sur la portion déjà traitée ».
- Vérifie l'initialisation : l'invariant est-il vrai juste avant la boucle (portion traitée vide) ?
- Vérifie la conservation : en supposant l'invariant vrai en début de tour, l'est-il encore après une exécution du corps ?
- Combine l'invariant avec la condition d'arrêt : c'est ce qui donne la postcondition, donc la correction.
4. Variant : prouver la terminaison
Un variant d'une boucle est une quantité entière qui reste positive ou nulle tant que la boucle s'exécute et qui décroît strictement à chaque tour. Un entier positif ne peut pas décroître indéfiniment : son existence prouve que la boucle termine.
Reprenons la boucle maximum. Posons . Au début d'un tour, la
condition i < len(t) assure v \geqslant 1 > 0. Chaque tour fait i = i + 1,
donc v diminue exactement de 1 : il est entier, positif et strictement décroissant. La
boucle fait donc au plus len(t) - 1 tours puis s'arrête.
d - g pour la dichotomie, len(t) - i pour un parcours, n - i pour
une somme, la valeur elle-même pour un compte à rebours. Cherche ce qui rapproche de la sortie.
i qui saute la valeur d'arrêt)
ne garantit rien : c'est la source la plus fréquente de boucle infinie.
5. Correction partielle et correction totale
Une fonction est partiellement correcte si, lorsqu'elle termine, son résultat vérifie la postcondition (« si ça s'arrête, le résultat est bon »). Elle est totalement correcte si elle est partiellement correcte et qu'elle termine toujours (sous la précondition).
Ces deux notions se prouvent avec deux outils distincts :
- L'invariant (+ condition d'arrêt) prouve la correction partielle.
- Le variant prouve la terminaison.
D'où l'égalité à retenir : correction totale = correction partielle + terminaison, soit invariant + variant.
6. Exemple entièrement traité : la somme 0+1+...+n
Prouvons complètement, du variant à la conclusion, la fonction qui calcule par une boucle.
def somme(n):
# Precondition : n est un entier, n >= 0
# Postcondition : renvoie 0 + 1 + ... + n
s = 0
i = 0
while i <= n:
s = s + i
i = i + 1
return ss = 0L'accumulateur. s stockera la somme construite au fur et à mesure. Une somme vide vaut 0.i = 0Le compteur. i est le prochain entier à ajouter. On commence à 0.while i <= n:On additionne jusqu'à n inclus. La condition est <= (et non <) car on veut ajouter n lui-même.s = s + iOn ajoute i à la somme. C'est le tour qui fait grossir l'accumulateur.i = i + 1La progression. On passe à l'entier suivant ; c'est ce qui fera décroître le variant et terminer la boucle.| Tour | i | s avant | s après | variant n − i |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 4 |
| 2 | 1 | 0 | 1 | 3 |
| 3 | 2 | 1 | 3 | 2 |
| 4 | 3 | 3 | 6 | 1 |
| 5 | 4 | 6 | 10 ✓ | 0 |
Pour tout entier n tel que , l'appel somme(n) termine et
renvoie .
Démonstration
On note P(i) l'invariant : (la somme des entiers de 0 à
i − 1, avec la convention qu'une somme vide vaut 0).
Initialisation. Avant le premier tour, i = 0 et s = 0.
L'invariant P(0) affirme (somme vide) , ce qui est vrai.
Conservation. Supposons P(i) vrai au début d'un tour, avec
: . Le corps exécute d'abord s = s + i,
d'où , puis i = i + 1, d'où
. On a bien : P(i') est vrai. L'invariant
est préservé.
Terminaison. Posons le variant . Au début d'un tour, la condition
i <= n donne . Chaque tour fait i = i + 1, donc
diminue exactement de 1 : c'est un entier positif strictement décroissant. Il ne peut décroître
indéfiniment, donc la boucle s'arrête (après tours).
Conclusion (correction). À la sortie, la condition i <= n est
fausse ; comme i augmente de 1 en 1 et valait n au dernier tour effectué,
on a exactement i = n + 1. Par l'invariant P(n+1) :
. Le résultat vérifie la postcondition. La boucle
terminant (variant) et le résultat étant correct (invariant), la fonction est
totalement correcte.
« Initialisation, conservation, terminaison, conclusion » : tu récites mais tu bloques à l'écrit ? Nos mentors te font rédiger la preuve complète sur cinq boucles-types (somme, max, factorielle, dichotomie, Euclide) jusqu'à ce que le plan devienne automatique en DS.
Trouver un mentor →7. Pièges classiques
n − i » n'est pas un
invariant, « s = 0+…+(i−1) » n'est pas un variant.
i = n + 1), la preuve de correction est incomplète.
i != n avec un pas de 2), elle peut passer sous
zéro sans jamais déclencher la sortie : la boucle ne termine pas. Le variant doit rester
tant que la boucle tourne.
8. Exercices d'application
Rédige-les sur papier avant d'ouvrir le corrigé, puis passe au quiz en bas de fiche.
On considère la fonction ci-dessous. Donne un invariant de la boucle, puis déduis-en ce que renvoie puissance(x, n).
def puissance(x, n):
# Precondition : n >= 0
p = 1
k = 0
while k < n:
p = p * x
k = k + 1
return pVoir la correction détaillée
k = 0 et p = 1, donc . Vrai.p = p * x on a , puis k = k + 1 redonne pour le nouveau k.k < n devient fausse avec k = n, donc . La fonction renvoie . (Variant : n − k.)Pour la boucle d'Euclide ci-dessous, propose un variant entier positif strictement décroissant et conclus quant à la terminaison.
def pgcd(a, b):
# Precondition : a >= 0, b >= 0, (a, b) != (0, 0)
while b != 0:
a, b = b, a % b
return aVoir la correction détaillée
b), qui est un entier .b est a % b, le reste de la division par b : il est strictement inférieur à b (car 0 \leqslant a \bmod b < b). Donc v décroît strictement.b finit par valoir 0, la condition b != 0 devient fausse et la boucle termine.Prouve la correction totale de la fonction suivante, censée calculer la factorielle (avec ). Énonce un invariant, un variant, et conclus.
def factorielle(n):
# Precondition : n >= 0
f = 1
k = 1
while k <= n:
f = f * k
k = k + 1
return fVoir la correction détaillée
k = 1, f = 1 = 0!. Vrai.f = f * k on a , puis k = k + 1 redonne pour le nouveau k.k <= n donne v \geqslant 1 > 0 ; chaque tour augmente k de 1, donc v décroît de 1. Entier positif strictement décroissant : la boucle s'arrête.k = n + 1, donc par l'invariant . Correction partielle (invariant) + terminaison (variant) = correction totale.Récap final — Ce qu'il faut absolument retenir
À la veille d'une khôlle ou d'un DS, parcours cette checklist : tu dois pouvoir répondre « oui, sans hésiter » à chaque question.
- Sais-tu distinguer précondition (hypothèse sur les arguments) et postcondition (garantie sur le résultat) ?
- Sais-tu à quoi sert
assertet pourquoi ce n'est pas unif? - Sais-tu qu'un invariant prouve la correction et un variant la terminaison ?
- Sais-tu qu'un variant est un entier qui décroît strictement à chaque tour ?
- Sais-tu vérifier un invariant en trois temps (initialisation, conservation, sortie) ?
- Sais-tu que correction totale = correction partielle + terminaison ?
- Sais-tu rédiger une preuve complète de boucle sans oublier le cas de sortie ?
- Sais-tu qu'une trace « qui marche » n'est pas une preuve ?
À savoir refaire
- Correction totale de la somme 0+1+…+n — invariant , variant , conclusion en
i = n+1.