Je lis "Introduction à l'algorithme" par CLRS. Au chapitre 2, les auteurs mentionnent les "invariants de boucle". Qu'est-ce qu'un invariant de boucle?
En termes simples, un invariant de boucle est un prédicat (condition) qui vaut pour chaque itération de la boucle. Par exemple, regardons une simple boucle for
qui ressemble à ceci:
int j = 9;
for(int i=0; i<10; i++)
j--;
Dans cet exemple, il est vrai (pour chaque itération) que i + j == 9
. Un invariant plus faible qui est également vrai est que i >= 0 && i <= 10
.
J'aime cette définition très simple: ( source )
Un invariant de boucle est une condition [parmi les variables de programme] qui est nécessairement vraie immédiatement avant et immédiatement après chaque itération d'une boucle. (Notez que cela ne dit rien sur sa vérité ou sa fausseté au milieu d'une itération.)
En soi, un invariant de boucle ne fait pas grand chose. Cependant, étant donné un invariant approprié, il peut être utilisé pour aider à prouver l'exactitude d'un algorithme. L'exemple simple dans CLRS concerne probablement le tri. Par exemple, laissez votre invariant de boucle être quelque chose comme, au début de la boucle, les premières entrées i
de ce tableau sont triées. Si vous pouvez prouver qu'il s'agit bien d'un invariant de boucle (c'est-à-dire qu'il tient avant et après chaque itération de boucle), vous pouvez l'utiliser pour prouver la validité d'un algorithme de tri: à la fin de la boucle, l'invariant de boucle est toujours satisfait , et le compteur i
est la longueur du tableau. Par conséquent, les premières entrées i
sont triées, ce qui signifie que tout le tableau est trié.
Un exemple encore plus simple: Boucles Invariants, exactitude et dérivation de programme .
D'après moi, un invariant de boucle est un outil systématique et formel permettant de raisonner sur les programmes. Nous faisons une seule déclaration que nous cherchons à prouver vraie, et nous l'appelons invariant de boucle. Cela organise notre logique. Bien que nous puissions tout aussi bien argumenter de manière informelle sur la correction d'un algorithme, l'utilisation d'un invariant de boucle nous oblige à réfléchir très soigneusement et garantit que notre raisonnement est hermétique.
Il y a une chose que beaucoup de gens ne réalisent pas tout de suite lorsqu'il s'agit de boucles et d'invariants. Ils sont confus entre l'invariant de boucle et la boucle conditionnelle (la condition qui contrôle la terminaison de la boucle).
Comme les gens le soulignent, l'invariant de boucle doit être vrai
(bien que cela puisse être temporairement faux pendant le corps de la boucle). D'autre part, la boucle conditionnelle doit être fausse une fois la boucle terminée, sinon la boucle ne se terminerait jamais.
Ainsi, l'invariant de boucle et la boucle conditionnelle doivent être des conditions différentes.
Un bon exemple d'invariant de boucle complexe concerne la recherche binaire.
bsearch(type A[], type a) {
start = 1, end = length(A)
while ( start <= end ) {
mid = floor(start + end / 2)
if ( A[mid] == a ) return mid
if ( A[mid] > a ) end = mid - 1
if ( A[mid] < a ) start = mid + 1
}
return -1
}
Ainsi, la boucle conditionnelle semble plutôt simple - lorsque début> fin, la boucle se termine. Mais pourquoi la boucle est-elle correcte? Quel est l'invariant de boucle qui prouve sa correction?
L'invariant est la déclaration logique:
if ( A[mid] == a ) then ( start <= mid <= end )
Cette déclaration est une tautologie logique - elle est toujours vraie dans le contexte de la boucle/de l’algorithme spécifique que nous essayons de prouver. Et il fournit des informations utiles sur l’exactitude de la boucle une fois celle-ci terminée.
Si nous retournons parce que nous avons trouvé l'élément dans le tableau, alors l'instruction est clairement vraie, puisque si A[mid] == a
, alors a
est dans le tableau et mid
doit être entre début et fin. Et si la boucle se termine parce que start > end
, il ne peut y avoir aucun nombre tel que start <= mid
et mid <= end
et nous savons donc que la déclaration A[mid] == a
doit être fausse. Cependant, l’instruction logique globale est toujours vraie au sens null. (En logique, l'affirmation si (faux) alors (quelque chose) est toujours vraie.)
Maintenant, qu'en est-il de ce que j'ai dit à propos de la boucle conditionnelle étant nécessairement fausse lorsque la boucle se termine? Il semble que lorsque l'élément est trouvé dans le tableau, la condition de boucle est vraie lorsque la boucle se termine!? En fait, ce n'est pas le cas, car la condition de boucle implicite est vraiment while ( A[mid] != a && start <= end )
, mais nous raccourcissons le test car la première partie est implicite. Cette condition est clairement fausse après la boucle, quelle que soit la façon dont la boucle se termine.
Les réponses précédentes ont très bien défini un invariant de boucle.
Permettez-moi maintenant d’expliquer comment les auteurs de CLRS ont utilisé des invariants de boucle pour prouver l’exactitude du type d’insertion.
Algorithme de tri d'insertion (comme indiqué dans le livre):
INSERTION-SORT(A)
for j ← 2 to length[A]
do key ← A[j]
// Insert A[j] into the sorted sequence A[1..j-1].
i ← j - 1
while i > 0 and A[i] > key
do A[i + 1] ← A[i]
i ← i - 1
A[i + 1] ← key
Invariant de boucle dans ce cas (Source: livre CLRS): Le sous-tableau [1 à j-1] est toujours trié.
Vérifions maintenant cela et prouvons que cet algorithme est correct.
Initialisation : Avant la première itération j = 2. Donc, Subarray [1: 1] est le tableau à tester. Comme il n’a qu’un élément, il est donc trié.Avariant est donc satisfait.
Maintanence : Ceci peut être facilement vérifié en vérifiant l'invariant après chaque itération. Dans ce cas, il est satisfait.
Termination : C'est l'étape où nous allons prouver l'exactitude de l'algorithme.
Lorsque la boucle se termine, la valeur de j = n + 1. De nouveau, l'invariant de boucle est satisfait. Cela signifie que Subarray [1 à n] doit être trié.
C'est ce que nous voulons faire avec notre algorithme. Ainsi, notre algorithme est correct.
En plus de toutes les bonnes réponses, je pense qu’un bon exemple tiré deComment penser aux algorithmes, de Jeff Edmondspeut très bien illustrer le concept:
EXEMPLE 1.2.1 "Algorithme Find-Max à deux doigts"
1) Spécifications: une instance d’entrée consiste en une liste L(1..n) de éléments. La sortie consiste en un index i tel que L(i) ait un maximum de valeur. S'il existe plusieurs entrées avec la même valeur, alors n'importe lequel l'un d'eux est retourné.
2) Étapes de base: Vous choisissez la méthode à deux doigts. Votre doigt droit descend la liste.
3) Mesure du progrès: La mesure du progrès est la mesure dans laquelle le liste votre doigt droit est.
4) L'invariant de boucle: L'invariant de boucle indique que votre doigt gauche pointe sur l'une des plus grosses entrées rencontrées jusqu'à présent par votre doigt droit.
5) Principales étapes: à chaque itération, vous déplacez votre doigt droit vers le bas d’un entrée dans la liste. Si votre doigt droit pointe maintenant vers une entrée plus grand que l’entrée du doigt gauche, puis déplacez votre gauche doigt pour être avec votre doigt droit.
6) Faire des progrès: vous faites des progrès parce que votre doigt droit bouge une entrée.
7) Maintain Loop Invariant: vous savez que l'invariant de boucle a été maintenu comme suit. Pour chaque étape, le nouvel élément doigt gauche est Max (ancien élément de doigt gauche, nouvel élément). Par l'invariant de boucle, c'est Max (Max (liste plus courte), nouvel élément). Mathématiquement, c'est Max (liste plus longue).
8) Établissement de l'invariant de boucle: Vous établissez initialement l'invariant de boucle en pointant les deux doigts vers le premier élément.
9) Condition de sortie: Vous avez terminé lorsque votre doigt droit est terminé traverser la liste.
10) Fin: En fin de compte, nous savons que le problème est résolu comme suit. Par la condition de sortie, votre doigt droit a rencontré tous les les entrées. En invariant de la boucle, votre doigt gauche pointe au maximum de ceux-ci. Renvoie cette entrée.
11) Terminaison et temps d'exécution: Le temps requis est constant fois la longueur de la liste.
12) Cas spéciaux: Vérifiez ce qui se passe quand il y a plusieurs entrées avec la même valeur ou lorsque n = 0 ou n = 1.
13) Détails de codage et de mise en oeuvre: ...
14) Preuve formelle: La justesse de l'algorithme découle de la étapes ci-dessus.
Il convient de noter qu'un invariant de boucle peut aider à la conception d'algorithmes itératifs lorsqu'il est considéré comme une assertion exprimant des relations importantes entre les variables qui doivent être vraies au début de chaque itération et à la fin de la boucle. Si cela est le cas, le calcul est sur la voie de l'efficacité. Si false, alors l'algorithme a échoué.
Invariant dans ce cas signifie une condition qui doit être vraie à un certain point de chaque itération de boucle.
Dans la programmation par contrat, un invariant est une condition qui doit être vraie (par contrat) avant et après l'appel de toute méthode publique.
La signification de l'invariant n'est jamais changée
L'invariant de boucle signifie ici "le changement qui survient dans la boucle (incrément ou décrément) ne change pas la condition de boucle, c'est-à-dire que la condition est satisfaisante", de sorte que le concept d'invariant de boucle est venu
Désolé je n'ai pas la permission de commenter.
@ Thomas Petricek comme vous l'avez mentionné
Un invariant plus faible qui est également vrai est que i> = 0 && i <10 (car il s'agit de la condition de continuation!) "
Comment c'est un invariant de boucle?
J'espère ne pas me tromper, autant que je sache[1], L’invariant de boucle sera vrai au début de la boucle (initialisation), il sera vrai avant et après chaque itération (Maintenance) et il sera également vrai après la fin de la boucle (Termination). Mais après la dernière itération, i devient 10. Donc, la condition i> = 0 && i <10 devient fausse et termine la boucle. Il viole la troisième propriété (Termination) de l'invariant de boucle.
[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html
Il est difficile de garder une trace de ce qui se passe avec les boucles. Les boucles qui ne se terminent pas ou qui ne se terminent pas sans atteindre le comportement souhaité sont un problème courant en programmation informatique. Les invariants de boucle aident. Un invariant de boucle est une déclaration formelle sur la relation entre les variables de votre programme qui est vérifiée juste avant que la boucle ne soit exécutée (établissement de l'invariant) et qui est vérifiée à nouveau au bas de la boucle, à chaque fois dans la boucle (maintien de l'invariant ) . Voici le schéma général d'utilisation des invariants de boucle dans votre code:
... // l'invariant de boucle doit être vrai ici
while (TEST CONDITION) {
// haut de la boucle
...
// bas de la boucle
// l'invariant de boucle doit être vrai ici
}
// Termination + Loop Invariant = Goal
...
Entre le haut et le bas de la boucle, des progrès sont vraisemblablement accomplis vers la réalisation du but de la boucle. Cela pourrait perturber (rendre faux) l'invariant. Le point des invariants de boucle est la promesse que l'invariant sera restauré avant de répéter le corps de la boucle à chaque fois . Il y a deux avantages à cela:
Le travail n'est pas reporté au passage suivant de manière complexe et dépendante des données. Chaque passage dans la boucle est indépendant de tous les autres, l’invariant servant à lier les passages ensemble en un ensemble fonctionnel . Le raisonnement que votre boucle fonctionne est réduit au raisonnement selon lequel l’invariant de boucle est restauré à chaque passage dans la boucle. Cela divise le comportement global complexe de la boucle en petites étapes simples, chacune pouvant être considérée séparément . La condition de test de la boucle ne fait pas partie de l'invariant. C'est ce qui fait que la boucle se termine. Vous considérez séparément deux choses: pourquoi la boucle doit toujours se terminer et pourquoi la boucle atteint son objectif lorsqu'elle se termine. La boucle se termine si, à chaque fois, vous vous approchez de la condition de fin. Il est souvent facile de s’assurer de cela: par exemple incrémenter d’un compteur variable jusqu’à atteindre une limite supérieure fixe. Parfois, le raisonnement derrière la résiliation est plus difficile.
L'invariant de boucle doit être créé pour que, lorsque la condition de terminaison est atteinte et que l'invariant soit vrai, l'objectif soit atteint:
invariant + terminaison => objectif
Il faut de la pratique pour créer des invariants simples et relatifs qui captent tous les objectifs, sauf la fin. Il est préférable d'utiliser des symboles mathématiques pour exprimer les invariants de boucle, mais lorsque cela conduit à des situations trop compliquées, nous nous appuyons sur une prose claire et sur le bon sens.
La propriété Invariant de boucle est une condition qui s'applique à chaque étape de l'exécution d'une boucle (c'est-à-dire, pour les boucles, les boucles while, etc.)
Ceci est essentiel pour une preuve d'invariant de boucle, où il est possible de montrer qu'un algorithme s'exécute correctement si, à chaque étape de son exécution, cette propriété d'invariant de boucle est vérifiée.
Pour qu'un algorithme soit correct, l'invariant de boucle doit tenir à:
Initialisation (le début)
Maintenance (chaque étape après)
Résiliation (quand c'est fini)
Ceci est utilisé pour évaluer un tas de choses, mais le meilleur exemple est celui des algorithmes gloutons pour la traversée de graphes pondérés. Pour qu'un algorithme glouton fournisse une solution optimale (un chemin sur le graphique), il doit atteindre tous les nœuds connectés dans le chemin le plus léger possible.
Ainsi, la propriété d'invariant de boucle est que le chemin emprunté a le moins de poids. Au début nous n’avons ajouté aucun bord, cette propriété est donc vraie (elle n’est pas fausse dans ce cas). À à chaque étape , nous suivons le bord de poids le plus faible (l’étape gloutonne), nous prenons donc à nouveau le chemin du poids le plus bas. À la fin , nous avons trouvé le chemin pondéré le plus bas, notre propriété est donc également vraie.
Si un algorithme ne le fait pas, nous pouvons prouver qu'il n'est pas optimal.
L'invariant de boucle est une formule mathématique telle que (x=y+1)
. Dans cet exemple, x
et y
représentent deux variables dans une boucle. Compte tenu du changement de comportement de ces variables tout au long de l'exécution du code, il est presque impossible de tester toutes les valeurs possibles pour les valeurs x
et y
et de voir si elles produisent un bogue. Disons que x
est un entier. Entier peut contenir 32 bits d’espace en mémoire. Si ce nombre dépasse, il se produit un débordement de mémoire tampon. Nous devons donc nous assurer que tout au long de l'exécution du code, cet espace ne dépasse jamais. pour cela, nous devons comprendre une formule générale qui montre la relation entre les variables. Après tout, nous essayons simplement de comprendre le comportement du programme.
En termes simples, il s'agit d'une condition LOOP qui est vraie à chaque itération de boucle:
for(int i=0; i<10; i++)
{ }
En cela, nous pouvons dire que l'état de i est i<10 and i>=0
Un invariant de boucle est une assertion qui est vraie avant et après l'exécution de la boucle.