Qu'est-ce qu'une boucle invariante?

268

Je lis "Introduction à l'algorithme" par CLRS. Dans le chapitre 2, les auteurs mentionnent les "invariants de boucle". Qu'est-ce qu'une boucle invariante?

Attilah
la source
4
Cela semble assez bon pour expliquer: cs.miami.edu/~burt/learning/Math120.1/Notes/LoopInvar.html
Tom Gullen
Au cas où quelqu'un voudrait résoudre un problème de codage algorithmique réel basé sur le concept d'invariant de boucle, veuillez vous référer à ce problème sur HackerRank. Ils ont également fait référence au problème de tri par insertion uniquement pour détailler le concept.
RBT
On peut également consulter les notes ici pour une compréhension théorique.
RBT

Réponses:

345

En termes simples, un invariant de boucle est un prédicat (condition) valable pour chaque itération de la boucle. Par exemple, regardons une forboucle simple 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 cela i >= 0 && i <= 10.

Tomas Petricek
la source
29
Ceci est un excellent exemple. Plusieurs fois, lorsque j'ai entendu un instructeur décrire l'invariant de la boucle, il s'agissait simplement de la «condition de boucle», ou quelque chose de similaire. Votre exemple montre que l'invariant peut être bien plus.
Brian S
77
Je ne vois pas cela comme un bon exemple car l'invariant de boucle devrait être quelque peu le but de la boucle ... CLRS l'utilise pour prouver l'exactitude d'un algorithme de tri. Pour le tri par insertion, en supposant que la boucle est itérative avec i, à la fin de chaque boucle, le tableau est ordonné jusqu'au i-ème élément.
Clash
5
oui, cet exemple n'est pas faux, mais juste pas assez. Je soutiens @Clash up, car l'invariant de boucle devrait présenter l'objectif, pas seulement pour lui-même.
Jack
7
@Tomas Petricek - lorsque la boucle se termine, i = 10 et j = -1; donc l'exemple invariant le plus faible que vous avez donné n'est peut-être pas correct (?)
Raja
7
Bien que je sois d'accord avec les commentaires ci-dessus, j'ai surévalué cette réponse parce que ... l'objectif n'est pas défini ici. Définissez un objectif qui vous convient et l'exemple est génial.
Flavius
119

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 cours 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 a probablement à voir avec le tri. Par exemple, laissez votre boucle invariante être quelque chose comme, au début de la boucle, les premières ientrées 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 l'exactitude d'un algorithme de tri: à la fin de la boucle, l'invariant de boucle est toujours satisfait et le compteur iest la longueur du tableau. Par conséquent, les premières ientrées sont triées signifie que l'ensemble du tableau est trié.

Un exemple encore plus simple: invariants de boucles, correction et dérivation de programme .

La façon dont je comprends un invariant de boucle est un outil formel systématique pour raisonner sur les programmes. Nous faisons une seule déclaration que nous nous concentrons à prouver vrai, et nous l'appelons l'invariant de boucle. Cela organise notre logique. Bien que nous puissions tout aussi bien argumenter de manière informelle sur l'exactitude de certains algorithmes, l'utilisation d'un invariant de boucle nous oblige à réfléchir très attentivement et garantit que notre raisonnement est hermétique.

TNi
la source
10
Il convient de noter que «immédiatement après chaque itération» inclut après la fin de la boucle - quelle que soit la façon dont elle s'est terminée.
Robert S.Barnes
Merci beaucoup pour cette réponse! La plus grande prise de celui-ci est que le fait d'avoir cette boucle invariante est d'aider à prouver l'exactitude de l'algorithme. Les autres réponses se concentrent uniquement sur ce qui est un invariant de boucle!
Neekey
39

Il y a une chose que beaucoup de gens ne réalisent pas tout de suite lorsqu'ils traitent de boucles et d'invariants. Ils se confondent entre l'invariant de boucle et la boucle conditionnelle (la condition qui contrôle la fin de la boucle).

Comme les gens le soulignent, l'invariant de boucle doit être vrai

  1. avant le début de la boucle
  2. avant chaque itération de la boucle
  3. après la fin de la boucle

(bien que cela puisse être temporairement faux pendant le corps de la boucle). D'un autre côté, la boucle conditionnelle doit être fausse après la fin de la boucle, sinon la boucle ne se terminerait jamais.

Ainsi la boucle invariante et la boucle conditionnelle doivent être des conditions différentes.

Un bon exemple d'invariant de boucle complexe est pour 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 assez simple - lorsque start> end la boucle se termine. Mais pourquoi la boucle est-elle correcte? Quel est l'invariant de boucle qui prouve son exactitude?

L'invariant est l'énoncé 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 / algorithme spécifique que nous essayons de prouver . Et il fournit des informations utiles sur l'exactitude de la boucle après sa fin.

Si nous revenons parce que nous avons trouvé l'élément dans le tableau, alors la déclaration est clairement vraie, car si A[mid] == aalors se atrouve dans le tableau et middoit être entre le début et la fin. Et si la boucle se termine parce que start > endalors il ne peut y avoir nombre tel que start <= mid et mid <= end par conséquent , nous savons que la déclaration A[mid] == adoit être fausse. Cependant, par conséquent, l'énoncé logique global est toujours vrai dans le sens nul. (En logique, l'énoncé si (faux) alors (quelque chose) est toujours vrai.)

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 boucle conditionnelle est vraie lorsque la boucle se termine!? Ce n'est pas le cas, car la boucle implicite est vraiment conditionnelle, while ( A[mid] != a && start <= end )mais nous raccourcissons le test réel 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.

Robert S. Barnes
la source
Il est étrange d'utiliser une instruction logique comme invariant de boucle, car comme toute instruction logique peut toujours être vraie, quelle que soit la condition.
acgtyrant le
Pas si étrange que je pense, car il n'y a aucune garantie qui aest présente dans A. De manière informelle, ce serait "si la clé aest présente dans le tableau, elle doit se produire entre startet endinclus". Il s'ensuit alors que si A[start..end]est vide, ce an'est pas présent dans A.
scanny
33

Les réponses précédentes ont très bien défini un invariant de boucle.

Voici comment les auteurs de CLRS ont utilisé l'invariant de boucle pour prouver l' exactitude du tri par insertion.

Algorithme de tri par 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

Boucle invariante dans ce cas: le sous-tableau [1 à j-1] est toujours trié.

Vérifions maintenant cela et prouvons que l'algorithme est correct.

Initialisation : Avant la première itération j = 2. Le sous-tableau [1: 1] est donc le tableau à tester. Comme il n'a qu'un seul élément, il est donc trié. L'invariant est donc satisfait.

Maintenance : Ceci peut être facilement vérifié en vérifiant l'invariant après chaque itération. Dans ce cas, il est satisfait.

Terminaison : C'est l'étape où nous prouverons l'exactitude de l'algorithme.

Lorsque la boucle se termine, la valeur de j = n + 1. L'invariant de boucle est à nouveau satisfait. Cela signifie que le sous-tableau [1 à n] doit être trié.

C'est ce que nous voulons faire avec notre algorithme. Notre algorithme est donc correct.

Tushar Kathuria
la source
1
D'accord ... la déclaration de résiliation est si importante ici.
Gaurav Aradhye
18

En plus de toutes les bonnes réponses, je suppose qu'un excellent exemple de How to Think About Algorithms, de Jeff Edmonds, peut très bien illustrer le concept:

EXEMPLE 1.2.1 "L'algorithme Find-Max à deux doigts"

1) Spécifications: une instance d'entrée se compose d'une liste L (1..n) d'éléments. La sortie est constituée d'un indice i tel que L (i) a une valeur maximale. S'il y a plusieurs entrées avec cette même valeur, alors n'importe laquelle d'entre elles est retournée.

2) Étapes de base: vous décidez de la méthode à deux doigts. Votre doigt droit parcourt la liste.

3) Mesure du progrès: La mesure du progrès est la distance de votre doigt droit le long de la liste.

4) L'invariant de boucle: L'invariant de boucle indique que votre doigt gauche pointe vers l'une des plus grandes entrées rencontrées jusqu'à présent par votre doigt droit.

5) Étapes principales: à chaque itération, vous déplacez votre doigt droit vers le bas d'une entrée de la liste. Si votre doigt droit pointe maintenant vers une entrée plus grande que l'entrée du doigt gauche, déplacez votre doigt gauche pour qu'il soit avec votre doigt droit.

6) Progresser: vous progressez parce que votre doigt droit déplace une entrée.

7) Maintenir l'invariant de boucle: Vous savez que l'invariant de boucle a été maintenu comme suit. Pour chaque étape, le nouvel élément de 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 a fini de parcourir la liste.

10) Fin: En fin de compte, nous savons que le problème est résolu comme suit. À la condition de sortie, votre doigt droit a rencontré toutes les entrées. Par l'invariant de la boucle, votre doigt gauche pointe au maximum de celles-ci. Renvoyez cette entrée.

11) Temps de terminaison et d'exécution: Le temps requis correspond à quelques fois la longueur de la liste.

12) Cas particuliers: vérifiez ce qui se passe lorsqu'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 œuvre: ...

14) Preuve formelle: l'exactitude de l'algorithme découle des étapes ci-dessus.

Vahid Rafiei
la source
Je pense que cette réponse "met vraiment le doigt" sur l'essentiel intuitif d'un invariant :).
scanny
6

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 qui exprime des relations importantes entre les variables qui doivent être vraies au début de chaque itération et lorsque la boucle se termine. Si tel est le cas, le calcul est sur la voie de l'efficacité. Si faux, l'algorithme a échoué.

Eric Steen
la source
5

Invariant dans ce cas signifie une condition qui doit être vraie à un certain point dans chaque itération de boucle.

Dans la programmation sous contrat, un invariant est une condition qui doit être vraie (par contrat) avant et après l'appel de toute méthode publique.

Mark Rushakoff
la source
4

Le sens de l'invariant n'est jamais changé

Ici, l'invariant de boucle signifie "Le changement qui arrive à la variable dans la boucle (incrémenter ou décrémenter) 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

sasidhar
la source
2

La propriété invariante de boucle est une condition qui s'applique à chaque étape de l'exécution d'une boucle (c'est-à-dire pour les boucles, tandis que les boucles, etc.)

Ceci est essentiel à une preuve invariante de boucle, où l'on est en mesure de montrer qu'un algorithme s'exécute correctement si à chaque étape de son exécution cette propriété invariante de boucle est respectée.

Pour qu'un algorithme soit correct, l'invariant de boucle doit tenir à:

Initialisation (le début)

Maintenance (chaque étape après)

Résiliation (lorsqu'elle est terminée)

Ceci est utilisé pour évaluer un tas de choses, mais le meilleur exemple est les algorithmes gourmands pour la traversée de graphe pondérée. Pour qu'un algorithme gourmand donne une solution optimale (un chemin à travers le graphique), il doit atteindre connecter tous les nœuds dans le chemin de poids le plus bas possible.

Ainsi, la propriété invariante de boucle est que le chemin emprunté a le moins de poids. Au début, nous n'avons ajouté aucun bord, donc cette propriété est vraie (ce n'est pas faux, dans ce cas). À chaque étape , nous suivons le bord de poids le plus bas (l'étape gourmande), donc encore une fois nous prenons le chemin de poids le plus bas. À la fin , nous avons trouvé le chemin pondéré le plus bas, donc notre propriété est également vraie.

Si un algorithme ne fait pas cela, nous pouvons prouver qu'il n'est pas optimal.

Alex Mapley
la source
1

Il est difficile de garder une trace de ce qui se passe avec les boucles. Les boucles qui ne se terminent pas ou ne se terminent pas sans atteindre leur comportement cible 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 reste vraie juste avant l'exécution de la boucle (établissement de l'invariant) et qui se vérifie à nouveau au bas de la boucle, chaque fois à travers la boucle (maintien de l'invariant ). Voici le schéma général de l'utilisation des invariants de boucle dans votre code:

... // le Loop Invariant doit être vrai ici
tandis que (TEST CONDITION) {
// haut de la boucle
...
// bas de la boucle
// le Loop Invariant doit être vrai ici
}
// Termination + Loop Invariant = Objectif
...
Entre le haut et le bas de la boucle, des progrès sont probablement faits pour atteindre l'objectif de la boucle. Cela pourrait perturber (fausser) l'invariant. Le point des invariants de boucle est la promesse que l'invariant sera restauré avant de répéter le corps de boucle à chaque fois. Il y a deux avantages à cela:

Le travail n'est pas reporté à l'étape suivante de manière compliquée et dépendante des données. Chaque passage à travers la boucle est indépendant de tous les autres, l'invariant servant à lier les passes ensemble dans un tout fonctionnel. Le raisonnement selon lequel votre boucle fonctionne est réduit au raisonnement selon lequel l'invariant de la boucle est restauré à chaque passage dans la boucle. Cela brise le comportement global compliqué 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-elle jamais se terminer et pourquoi la boucle atteint son objectif lorsqu'elle se termine. La boucle se terminera si, à chaque fois, vous vous rapprochez de la condition de terminaison. Il est souvent facile de le garantir: par exemple incrémenter une variable compteur d'une unité jusqu'à ce qu'elle atteigne une limite supérieure fixe. Parfois, le raisonnement derrière la résiliation est plus difficile.

L'invariant de boucle doit être créé de sorte que lorsque la condition de terminaison est atteinte et que l'invariant est vrai, le but est atteint:

invariant + terminaison => objectif
Il faut de la pratique pour créer des invariants simples et liés qui capturent tous les objectifs, à l'exception de la terminaison. Il est préférable d'utiliser des symboles mathématiques pour exprimer des invariants de boucle, mais lorsque cela conduit à des situations trop compliquées, nous nous appuyons sur une prose claire et du bon sens.


la source
1

Désolé, je n'ai pas la permission de commenter.

@Tomas Petricek comme vous l'avez mentionné

Un invariant plus faible qui est également vrai est que i> = 0 && i <10 (car c'est la condition de continuation!) "

Comment est-ce une boucle invariante?

J'espère que je ne me trompe pas, pour autant que je sache [1] , l'invariant de boucle sera vrai au début de la boucle (initialisation), ce sera vrai avant et après chaque itération (maintenance) et ce sera aussi vrai après la fin de la boucle (terminaison) . Mais après la dernière itération, i devient 10. Ainsi, la condition i> = 0 && i <10 devient fausse et termine la boucle. Il viole la troisième propriété (terminaison) de l'invariant de boucle.

[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html

Mahmudul Haque
la source
Je suppose que c'est vrai parce que la boucle ne s'exécute pas réellement dans ces conditions.
muiiu
0

L'invariant de boucle est une formule mathématique telle que (x=y+1). Dans cet exemple, xet yrepré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 tous les possible xet les yvaleurs et voir si elles produisent tout bug. Disons que xc'est un entier. Un entier peut contenir un espace de 32 bits dans la mémoire. Si ce nombre dépasse, un dépassement de tampon se produit. Nous devons donc être sûrs que tout au long de l'exécution du code, il ne dépasse jamais cet espace. pour cela, nous devons comprendre une formule générale qui montre la relation entre les variables. Après tout, nous essayons juste de comprendre le comportement du programme.

Mehmet YILMAZ
la source
0

En termes simples, c'est une condition LOOP qui est vraie dans 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

i.maddy
la source
0

Un invariant de boucle est une assertion qui est vraie avant et après l'exécution de la boucle.

Timothy Makobu
la source
-1

Dans la recherche linéaire (selon l'exercice donné dans le livre), nous devons trouver la valeur V dans un tableau donné.

C'est aussi simple que de scanner le tableau à partir de 0 <= k <longueur et de comparer chaque élément. Si V est trouvé ou si le balayage atteint la longueur du tableau, il suffit de terminer la boucle.

Selon ma compréhension du problème ci-dessus-

Invariants de boucle (initialisation): V n'est pas trouvé dans l'itération k - 1. Très première itération, ce serait -1 donc on peut dire que V ne se trouve pas à la position -1

Maintien: dans la prochaine itération, V non trouvé en k-1 est vrai

Terminaison: si V trouvé en position k ou k atteint la longueur du réseau, terminer la boucle.

AndroDev
la source