Un algorithme n'est correct à $90\%$, il est donc considéré incorrect.
Nous nous sommes demandés si il est possible de prouver qu'un algorithme est correct. Le logicien et mathématicien autrichien Kurt Gödel a démontré qu'aucun système formel suffisamment puissant pour décrire l'arithmétique ne peut prouver sa propre cohérence au sein de ce même système.
En France (mais pas que...), il existe un courant de recherche qui construit des programmes basés sur des algorithmes pour prouver des algorithmes.
Exemple
L'assistant de preuve Coq.
Coq est un assistant de preuve formelle. Cela signifie que c’est un logiciel qui aide à prouver mathématiquement la correction d’algorithmes ou de théorèmes.
Comment ça marche ?
On écrit le programme ou la propriété qu’on veut vérifier sous forme mathématique. Coq permet alors de construire une preuve pas à pas, et vérifie automatiquement que chaque étape est correcte.
À quoi ça sert ?
Vérifier la correction d’algorithmes : par exemple, s’assurer qu’un tri renvoie toujours une liste triée. Formaliser des théorèmes mathématiques complexes et prouver qu’ils sont corrects. Produire du code certifié à partir de ces preuves, qui est garanti correct par construction.
Si l’on fait un algorithme “soigné”, alors en théorie, il n’y a pas de problèmes pour que le programme le soit lui aussi. Malgré tout cela n’est pas toujours vrai, par contre avoir des bonnes bases est important pour faire un bon programme.
Définition
L'efficatité d'un algorithme c'est le fait de se demander si son programme nous donne une réponse dans un temps raisonnable.
Définition
Un programme informatique est un algorithme qui a été codé sur une machine à l'aide d'un langage spécifique.
Efficacité d'un algorithme
Si l'on dit qu'un certain algorithme $\mathcal{A}$ s'exécute en $10s$ ceci n'a pas de sens, comme expliqué au dessus, l'efficacité d'un algorithme dépend de la machine. Ainsi, lorsque l'on étudie l'efficacité d'un algorithme, on doit répondre à plusieurs questions :
Sur quelle machine ?
Pour quelle.s entrée.s
On considère que l’efficacité d’un algorithme se mesure grâce aux opérations que l’algorithme fait. Pour déterminer l’efficacité de ce dernier on va en fait calculer le nombre d’opérations effectuées.
Exemple
On considère l'algorithme $\mathcal{A}_1$ suivant :
Entrée : T un tableau d'entiers int
Sortie : Le minimum du tableau
Algorithme A
m = T[1]
for i in [2,N]
if m > T[i] : m = T[i]
return m
On cherche a évaluer l'efficacité de l'algorithme en fonction de N.
Déroulement de l'algorithme :
Initialement, on considère que le premier élément du tableau T est le minimum, d'où m = T[1].
On parcours ensuite le tableau du second élément jusqu'au dernier. On évite de vérifier le premier élément car on suppose que c'est le minimum. On va donc faire $N-1$ fois la boucle.
On compare l'entier courant du tableau et le minimum entre eux et on agit selon le résultat.
À la fin on retourne m, l'entier minimum du tableau.
Hypothèse : On considère que le tableau est de taille $N=10$ et qu'il est trié dans l'ordre croissant.
Par exemple on pourrait avoir :
T = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
Au début de l'algorithme $\mathcal{A}_1$, on associe à m le premier élément du tableau, ici 1.
Puisque le tableau contient $N=10$ élément, et qu'on effectue $N=1$ boucle, alors on va boucler $9$ fois, pour faire $9$ comparaisons entre le minimum courant et l'élément courant du tableau.
Le tableau T trié dans l'ordre croissant implique que toutes les comparaisons m>T[i] seront forcément fausse. On ne change donc jamais la valeur de m.
On retourne à la fin m=1.
On peut aussi se demander combien de contrôle on fait.
Définition
Un contrôle représente simplement un test, une comparaison exécutée par l'algorithme.
Pour notre algorithme $\mathcal{A}_1$ on compte : $10$ contrôles pour la boucle for.
Dans notre cas, l'opération appelée contrôle, représente le nombre de fois que l'on vérifie si $i$ est inférieur ou égal à $N$.
En gros, notre boucle vas de $2$ jusqu'à $N$. Ainsi pendant l'exécution la variable $i$ va prendre les valeurs suivantes :
$$i : 2 \to 3 \to 4 \to \ldots \to N \to N+1$$
Okay, c'est là que ça se complique, vous allez sûrement me demander : Mais pourquoi on doit prendre $N+1$ ?
En fait, lorsque $i=N$ la boucle for va s'exécuter car on vérifie dans notre algo si $i\leq N$. Ainsi après l'exécution du rang $i=N$, la boucle incrément $i$ à $N+1$ et vérifie donc une $N$-ième fois si $i$ est inférieur ou égal à $N$. Ici, la condition est dépassée c'est donc après ce contrôle que l'on sort de la boucle.
On a donc effectué $c=10$ opérations de contrôles pour ce cas pour l'algorithme $\mathcal{A}_1$.
Second cas de figure
Hypothèse : On considère un tableau de taille $N=10$ et qu'il est trié dans l'ordre décroissant.
Par exemple on pourrait avoir :
T = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }
Au début de l'algorithme, on considère que le minimum du tableau est le premier élément, on affecte donc à m l'entier $10$.
Le tableau contient $10$ éléments, et puisqu'il est trié dans l'ordre décroissant alors, la condition m>=T[i] sera toujours vraie ce qui implique que pour chaque tour de boucle, on va réaffecter à m l'entier courant du tableau. Au total pusiqu'on compare avec les éléments $2$ à $10$ il va y avoir $9$ comparaisons donc $9$ affectations.
L'algorithme renvoi m=1 le dernier élément du tableau.
Pour ce second cas de figure, on va avoir $1$ affectations au début de l'algorithme, $10$ contrôles pour la boucle for et $9$ affectations.
Ainsi on obtient : $10$ contrôles $+ 10$ affectations.
Conclusion de l'étude
Si on considère un tableau d'entiers T de N éléments.
On a $N-1$ comparaisons
On a $N$ contrôles
On a toujours $1$ retour
Dans le meilleur des cas on a $1$ seule affectation, et dans le pire des cas on en a $N$. Ainsi, on en conclu que le nombre d’affectation dépend de l’entrée même si l’entier N n’est pas fixé.
Avancer dans l'analyse algorithmique
Définitions et bases
Définition
Quelques éléments de vocabulaire et de notation de base concernant l'écriture et l'évaluation d'algorithmes.
On appelle opération élémentaire une action de base généralement exécutée en un temps constant notée $t$ qui dépend de la machine. Elles permettent de calculer la complexité d'un algorithme.
Les opérations arithmétiques, $+$, $-$, $*$, $/$, $DIV$, $MOD$
$DIV$ permet de déterminer le quotient d'une division euclidienne.
$MOD$ permet de déterminer le reste d'une division euclidienne.
Les comparaisons : $==$, $<=$, $>=$, $<$, $>$, $!=$ sur les types élémentaires uniquement !
Les opérateurs logiques
ET et OU prennent $2$ booléens
NON prend $1$ booléen
Les opérations de contrôles permettent de gérer les conditionnelles, le résultat.
Dans nos différentes études, on va considérer que la taille de l'entier est minimal, même si cela dépend en faite du codage utilisé.
Exemple
Considérons $n=14$.
en base 10, $n=14$. Le codage utilise $2$ chiffres.
en base $2$, $n=1110=2^3+2^2+2^1$. Le codage utilise $4$ chiffres.
en base $1$, $n=11111111111111$. Le codage utilise $14$ chiffres.
Ordre de grandeur
Définition
On appelle ordre de grandeur la tendance d'une courbe quand $N$ augmente.
En algorithmique, L’ordre de grandeur est utilisée pour évaluer la tendance du temps d’exécution (ou de l’espace mémoire) requis par un algorithme en fonction de la taille donnée. Cela permet en fait de classer les algorithmes selon leur efficacité sans se soucier des constantes ou des termes qui influent peu. L’ordre de grandeur est notée $O$, avec la notation Big-O qui donne une approximation asymptotique de la complexité.
Dans ce cours, on utilisera la notation de Landau $\mathcal{O}$ créée par Brechmann en 1894 et popularisée par Landau, qui permet de décrire le comportement des fonctions selon la tendance de $N$, pour objectif de déterminer la complexité de certains algorithmes.
Dans l’objectif de mieux comprendre la notation $\mathcal{O}$ de Landau, nous allons utiliser quelques exemples.
1
Propriété
À retenir
Dans le cadre de l'unité d'enseignement MATHÉMATIQUES POUR L'INFORMATIQUE 1, la notion de suite/fonction dominée a été introduite.
Soient $f$ et $g$ deux fonction, on dit que $f$ est dominée par $g$ si et seulement si :
$$\exists C > 0, \; \forall n_0 \geq n \quad f(n)\leq C g(n)$$
Dans ce cas on notera : $f(n) = O(g(n))$.
En français, on cherche un rang strictement positif $n_0$ à partir duquel $f$ est plus petit ou égal à $g$.
Théorème
Θ
Théorème fondamental
Soit $g$ une fonction quelconque. Il est alors évident qu'une fonction se domine elle-même. On note alors $$g=\mathcal{O}(g)$$
Soient $f$ et $g$ deux fonctions. $$(f=\Theta(g)) \Longleftrightarrow (f=\mathcal{O}(g) \land g=\mathcal{O}(f))$$
En gros, si $f=\Theta(g)$ ça revient à dire que les fonctions $f$ et $g$ se dominent mutuellement, autrement dit qu'elles croient de manière proportionnelle, qu'elles ont la même allure, la même complexité.
Complexité des algorithmes
Définition
Soit $\mathcal{A}$ un algorithme, on introduit ici trois complexités selon les cas :
Complexité au pire d'un algorithme $\mathcal{A}$ est la fonction qui permet de donner le nombre maximum d'opérations élémentaires qu'effectue un algorithme en fonction de $N$ la taille de l'entrée. Généralement, on donne pas une fonction mais un ordre de grandeur.
Complexité au mieux d'un algorithme $\mathcal{A}$ c'est la fonction qui permet de donner le nombre minimum d'opération élémentaires que doit faire un algorithme en fonction de $N$.
Complexité moyenne, est plus hypothétique, on connais la probabilité des entrées, on cherche alors une fonction qui donne le nombre moyen d'opérations élémentaires.
Pour la complexité d’une boucle for à $n$ itérations et $k$ (constant) opérations élémentaire On va avoir une complexité en $\mathcal{O}(nk)$ mais, puisque $k$ est une constante, on peut la négliger et on se retrouve dans le pire, le meilleure des cas et en moyenne à $\mathcal{O}(n)$.
Exemple
On considère les boucles suivantes :
for i de 1 à N-1
for j de 0 à N-i
Comparer T[j], T[j+1]
peut être changer j, j+1
Ici, on dispose de deux boucles : une sur i et une sur j. Puis on admet aussi deux opérations élémentaires qui s'effectue en un temps constant : comparer et changer.
Étudions les boucles de plus près :
Boucle externe
i va de 1 à N-1.
On boucle alors N-1 fois dans tous les cas.
Boucle interne
j va de 0 à N-i
Puisque j démarre de 0 alors on ajoute obligatoirement un tour de boucle supplémentaire quelque soit N.
Ainsi la seconde boucle va s'exécuter N-i+1 fois pour tous les N.
Un algorithme récursif est une méthode permettant de resourdre des problèmes en s’appelant elle-même, afin de résoudre des sous-problèmes du même type. généralement la récursivité est utilisé pour des problèmes qui veut être divisés en sous-problèmes similaires.
Un algorithme récursif nécessite :
Une condition d’arrêt, en gros c’est le moment ou l’algorithme arrête de s’appeler lui-même (sinon récursion infinie)
Appel récursif
Pour mieux comprendre la notion de récursivité prenons un exemple concret
Étude de cas - Recherche dichotomique
L'algorithme de recherche dichotomique(ou recherche binaire) est un algorithme efficace permettant de rechercher un élément dans un tableau trié. Il repose sur le principe des méthodes "diviser pour régner" en réduisant l'espace de recherche de moitié à chaque étape.
Principe de base :
On choisit un élément $x$ recherché
On se place au milieu du tableau trié
On récupère l'élément du milieu
Si $x=m$ on arrête de chercher, on a trouvé l'élément
Si $x<m$ alors il faut chercher à gauche du tableau.
Si $x>m$ alors il faut chercher à droite du tableau.
Lorsque les indices se croisent, ou si on a trouvé $x$, on s'arrête.
Algorithme
Entrées :
$D$ la liste triée
$x$ l'élément que l'on cherche
$deb$ l'indice de début de la partie du tableau dans laquelle on est
$fin$ l'indice de fin de la partie du tableau qu'on utilise
Sortie :
indice de l'élément $x$ si il est trouvé.
Algorithme rechDico(D, x, debut, fin)
if debut > fin
return -1 // croisement d'indice, élément non trouvé
milieu = (debut + fin) DIV 2
elemMiddle = D[milieu]
if elemMiddle = x
return milieu // élément trouvé, retourne sa position
else if x < elemMiddle
return rechDico(D, x, debut, milieu - 1) // chercher dans la moitié gauche
else
return rechDico(D, x, milieu + 1, fin) // chercher dans la moitié droite
Fin Algorithme
Étude de l'algorithme
Dans cet algorithme, la complexité du nombre d'OE (hors appels récursifs) ne sera pas la même selon els cas dans lesquels on se trouve.
Dans le meilleur des cas
Pour le cas le plus favorable, l'élément recherché $x$ se trouve au milieu de l'algorithme. Dans ces cas là on trouve directement l'élément. $$O(1)$$
Dans la moyenne
on ne peut pas déterminer car on a pas d’informations sur la taille des l’entrées ou autre. Pour ce faire il faudrait que l'on sache la probabilité des entrées par exemple.
Dans le pire des cas
L'élément recherché est :
Le premier élément du tableau.
Le dernier élément du tableau.
L'algorithme va alors découper le tableau de moitié à chaque appel récursif jusqu'à ce qu'il ne reste qu'un seul et unique élément à comparer pour enfin le trouver.
Ainsi dans le pire des cas la complexité de la recherche dichotomique est donnée par $$O(log(n))$$
Avec un nombre d'opération élémentaires qui suit une fonction $f(N)$ avec $N$ la taille de l'entrée, alors la complexité sera $NbAppels \times f(N)$. Le nombre d'opérations peut dépendre aussi de la taille des données traitées.
Exemple général
La taille des données par appels récursifs sont donnés par $N, N-1, N-2, ...$ de taille respectivement $T1, T2, ...$ et $f(T_i) = k \times T_i$(linéaire).
Alors la complexité au pire, lorsque l'on fait tous les appels de $1$ à $N$ est donnée par :
$$C = \sum_{i=1}^{N} k \times T_i = k \times \sum_{i=1}^{N} T_i = k \times N\times\frac{N+1}{2} = O(N^2)$$
On peut aussi avoir le cas où 2 appels sur une taille $T_i$ contiennent deux appels sur deux tailles différentes.
Étude de cas - Fibonacci
On considère la suite de Fibonacci définie par :
$$u_0=u_1=1 \quad\quad\quad \forall n \geq 2, \quad u_n=u_{n-1}+u_{n-2}$$
Ainsi, on obtient l'algorithmique récursif suivant :
Entrée :
$N$ le rang de la suite
Sortie :
le terme de rang $N$ de la suite
Algorithme fibo
if N = 0 or N = 1
return 1
else
return fibo(N - 1) + fibo(N - 2)
fin Algo
Alors pour un appel de rang $N$, on peut avoir deux appels $N-1$ puis $N-2$.
Ce qui abouti pour $N=6$ à l'arbre d'appels suivant :
Pour $N=6$ on fait $23$ appels
Pour $N=5$ on fait $12$ appels
Pour $N=4$ on fait $8$ appels
On peut conjecturer que le nombre d'appels devient exponentiel lorsque $N$ devient grand.
Finitude d'un algorithme
Introduction
Un algorithme est une suite finie et ordonnée d’instructions permettant de résoudre un problème ou d’accomplir une tâche. La notion de finitude est centrale dans cette définition : un algorithme doit nécessairement se terminer après un nombre fini d’étapes.
La finitude distingue l’algorithme d’un simple processus ou d’une procédure indéfinie. Un programme qui boucle sans fin, même s’il suit des règles précises, n’est pas un algorithme au sens strict, car il ne produit jamais de résultat final. Ainsi, garantir la terminaison est une condition fondamentale pour pouvoir juger de la correction et de l’efficacité d’un algorithme.
Cette exigence soulève des questions théoriques importantes. Par exemple, peut-on toujours savoir à l’avance si un algorithme se terminera ? Les travaux d’Alan Turing ont montré que ce n’est pas le cas : le problème de l’arrêt prouve qu’il est impossible, en général, de décider automatiquement si un algorithme quelconque finira son exécution. La finitude n’est donc pas seulement une contrainte pratique, mais aussi une limite fondamentale du calcul.
En pratique, la finitude est liée à la conception même des algorithmes : choix des conditions d’arrêt, bornes sur les boucles, diminution progressive des données traitées. Elle permet d’assurer que les ressources (temps, mémoire) restent contrôlées et que le résultat attendu sera effectivement produit.
Ainsi, la finitude des algorithmes constitue à la fois un principe fondateur de l’informatique, une exigence méthodologique pour le programmeur, et un enjeu théorique majeur dans l’étude des capacités et des limites du calcul automatique.
Définition
Un variant est une valeur qui diminue à chaque étape d’un algorithme et qui permet de montrer que l’algorithme va finir.
Exemple
Considérons l'algorithme suivant :
tant que n > 0 :
n = n - 1
n reste positif et diminue à chaque tour de boucle, il est alors le variant de notre algorithme.
Pour montrer qu’un algorithme est fini (= terminaison) il faut analyser s’il comporte un nombre fini d’étapes et s’il s’arrête après un temps fini pour toutes les entrées possibles.
Définition
On dit d’un algorithme qu’il est fini s’il se termine après un nombre fini d’étapes, quelque soit l’entrée choisie.
Objectifs :
Identifier un variant
Vérifier l'absence de boucles infinies
S'assurer que à chaque "branche" de l'algo mène à une terminaison.
Le dilemme de Achille et la tortue
En théorie
Considérons l'algorithme suivant :
var D = LT - LA
tant que D > 0
LA = LA+D
LT = LT + D/2
D = LT-LA
retourner LA
Dans notre algorithme, $D=L_T-L_A$. À chaque itérations Achille avance de $D$ la tortue avance de $D/2$. La nouvelle distance entre Achille et la Tortue est donnée par :
$$D_{new} = (L_T+D/2)-(L_A+D)=D/2$$
La distance est divisée par $2$ à chaque tour de boucle.
La distance tend vers $0$ mais sera toujours plus grande. Ainsi la condition : D > 0 dans la boucle tant que est toujours vraie l'algorithme ne se terminera donc jamais.
Achille ne rattrapera JAMAIS la tortue.
En pratique
En pratique, on ne manipule pas des réels mathématiques, mais des nombres flottants.
Problème clé : la précision est finie
Un double en Java a une précision limitée $\sim 10^{-16}$, et ne peut pas représenter des valeurs arbitrairement petites.
À un moment donné $D$ devient si petit qu’il est :
arrondi à $0$
ou considéré comme $0$ par la machine
Donc la boucle suivante devient fausse.
while (D > 0)
L'algorithme se termine, ce qui signifie qu'Achille rattrape la tortue numériquement mais pas mathématiquement comme expliqué au dessus.
Correction d'un algorithme
La correction d’un algorithme désigne la propriété selon laquelle un algorithme produit systématiquement le résultat attendu. Autrement dit, un algorithme est dit correct lorsqu’il fournit toujours une solution correcte pour toute les entrées valides.
Définition
On dit qu’un algorithme est partiellement correct si il fourni un résultat correct lorsqu’il finit forcément par se terminer (= finitude). Mais, cette notion ne garantit pas toujours que l’algorithme se termine.
On dit qu’un algorithme est totalement correct quand il est à la fois partiellement correct et qu’il termine pour toutes les entrées possibles. Autrement dit un algorithme est correct lorsqu’il se finit et fournit des sorties valides pour toutes entrées valides.
Vérifier la correction d’un algorithme, c’est montrer qu’il fait ce qu’il est censé faire, pour toutes les entrées valides. Il existe plusieurs méthodes pour le démontrer formellement.
Raisonnement par récurrence
Cette méthode est surtout utilisée pour les algorithmes récursifs ou itératifs.
Elle se déroule en trois étapes :
Cas de base : On montre que l'algorithme fonctionne correctement pour la plus petite valeur possible.
Hypothèse de récurrence : On suppose que l'algorithme fonctionne correctement pour un cas arbitraire $k$
Récurrence : On démontrer alors que si l'algorithme fonctionne pour $k$ alors il fonctionne aussi pour $k+1$.
Si les trois étapes sont vérifiées, l'algorithme est correct pour toutes les valeurs $k$ possible.
Invariant de boucle
Cette méthode s’applique aux algorithmes qui utilisent des boucles.
Un invariant de boucle est une propriété qui est :
vraie avant l’entrée dans la boucle,
vraie après chaque itération,
vraie à la fin de la boucle.
La démarche est la suivante :
Montrer que l’invariant est vrai avant la première itération.
Montrer que si l’invariant est vrai au début d’une itération, il reste vrai après l’itération.
Utiliser l’invariant et la condition d’arrêt pour montrer que le résultat final est correct.
Notation formelle : précondition et postcondition
Considérons un algorithme $\mathcal{A}$.
L'algorithme prend en entrée un élément $e$ appartenant à un ensemble de définition $D_e$.
L'algorithme renvoie une sortie $s$ appartenant à un ensemble $D_s$.
$Y$ une variable interne utilisée pendant l'exécution de l'algorithme.
Précondition : $\phi(e)$ est un prédicat vraie pour toutes les entrées $e \in D_e$. Elle décrit ce qui doit être vraie avant l'exécution de l'algorithme.
Postcondition : On note $\psi(e, s)$ un prédicat qui vérifie si la sortie $s$ est correcte pour l'entrée $e$. Elle décrit ce qui doit être vraie après l'exécution de l'algorithme.
Étude de cas - Algorithme de puissance de 2
On considère l'algorithme suivant :
Entrée : $x$ l'entier à tester si il est puissance de $2$
Sortie : un booléen permettant de dire si $x$ est une puissance de $2$.
Algo puissanceDeux
m = x
pair = VRAI
tant que (m>1) et pair
pair = m est pair ?
m = m DIV 2
retourner pair
Analyse de l'algorithme
On peut analyser l'algorithme.
Il prend en entrée un entier $x \in \mathbb{N}$ naturel.
Il renvoie un booléen qui indique si $x$ est une puissance de $2$.
Les étapes de l'algorithme :
On commence par associer à la variable m l'entier x testé et pair est initialisé à vrai. Tant que m est plus grand que $1$ et pair est vrai, on vérifie si m est pair et on associe le résultat du test dans la variable pair. On divise m par $2$. Lorsque l'on sorte de la boucle tant que, on retourne la valeur de pair.
Pourquoi peut-on dire que l'algorithme fonctionne ?
Par définition, un entier $x$ est une puissance de $2$ si il est positif strict et si il peut être diviser un certain nombre de fois par $2$ jusqu'à obtenir $1$ sans jamais devenir impair.
Pour $x=8$ on a : $$8 \to 4 \to 2 \to 1$$ on s'arrête car m est égal à $1$, et on renvoie vraie car $8$ est une puissance de $2$.
Pour $x=12$ on a : $$12 \to 6 \to 3$$ Puisqu'une des division par $2$ de m donne un entier impair plus grand que $1$ alors on s'arrête et on renvoie faux.
Invariant de boucle à chaque itération de la boucle tant que si pair reste à vrai alors cela signifie que tous les m précédents sont pairs, condition nécessaire pour qu'un nombre soit une puissance de $2$.
Terminaison la boucle tant que se termine lorsque m <= 1 ou lorsque pair = faux, c'est à dire une division par $2$ donne un résultat impair. On note $V=m$ le variant mis en jeu car il décroit à chaque tour de boucle d'au moins $1$, alors à un moment donné on aura forcément m <= 1 dans ce cas la boucle s'arrête et l'algorithme renvoie la valeur de la variable pair.
Conclusion, si l'algorithme se termine avec pair = vrai, $x$ sera forcément une puissance de $2$.
Correction totale
La variable m est divisée par $2$ à chaque tour de boucle, donc elle décroit strictement et attendra forcément une valeur plus petite que $1$ ou alors $1$ lui même, assurant la fin de la boucle tant que.
Cherchons un invariant...
Définition
On appelle invariant une propriété qui reste vraie tout le long de l’exécution d’une boucle ou d’un programme, à chaque itération.
Si on reprend la boucle tant que :
tant que (m>1) et pair
pair = m est pair ?
m = m DIV 2
On a :
m une variable divisée par $2$ à chaque itération.
$k$ un compteur représentant le nombre d'itérations effectuées.
On note $r$ la valeur qui représente le reste des divisions euclidiennes précédentes !
$r=1$ si la division donne un nombre impair
$r=0$ si la division donne une nombre pair
Ainsi pour $x=2^k$ avec $k \in \mathbb{N}$, si $x$ est une puissance de $2$ alors on obtient l'invariant suivant : $$I=m\times2^k+r \times 2^{k-1}$$
Preuve par récurrence
Essayons de montrer $I$ à l'aide d'une preuve par récurrence.
INITIALISATION au début de l'algorithme on a
$m=x$
$k=0$ et $r=0$
Ainsi $$I=m \times 2^{0}+r \times 2^{-1}=x \times 1+0\times 2^{-1}=x$$ l'invariant $I$ est donc correct au début de l'algorithme.
HYPOTHÈSE DE RÉCURRENCE on suppose que $I$ est vraie pour une certaine itération $k \in \mathbb{N}$. Montrons que $I$ est vraie pour l'itération suivante $k+1$.
RÉCURRENCE
On par de l'hypothèse de récurrence :
$$I=m_k2^k+r_k2^{k-1}$$
Pour le rang suivant on va diviser $m_k$ par 2 d'où : $$m_{k+1}=\left\lfloor\frac{m_k}{2}\right\rfloor \quad r_{k+1}=m_k \text{ MOD }2 \Longleftrightarrow m_k=2m_{k+1}+r_{k+1}$$
D'où on a :
$$\begin{align*}I&=m_k2^k+r_k2^{k-1}\\ &= (2m_{k+1}+r_{k+1})2^{k}+r_k2^{k-1}\\&=\underbrace{2^{k+1}m_{k+1}}_{\text{pEntière }i=k+1}+\underbrace{2^kr_{k+1}+r_k2^{k-1}}_{\text{reste de toutes les div}}\end{align*}$$
Ainsi à l'itération $k+1$ on retrouve bien le quotient de la division euclidienne de l’itération courante ainsi que le reste des itérations précédentes. La structure de l'invariant est globalement respectée à chaque itération.