Estoy leyendo "Introducción al algoritmo" por CLRS. En el capítulo 2, los autores mencionan "invariantes de bucle". ¿Qué es un bucle invariante?
Estoy leyendo "Introducción al algoritmo" por CLRS. En el capítulo 2, los autores mencionan "invariantes de bucle". ¿Qué es un bucle invariante?
Respuestas:
En palabras simples, un bucle invariante es un predicado (condición) que se cumple para cada iteración del bucle. Por ejemplo, veamos un for
bucle simple que se ve así:
int j = 9;
for(int i=0; i<10; i++)
j--;
En este ejemplo, es cierto (para cada iteración) que i + j == 9
. Un invariante más débil que también es cierto es eso
i >= 0 && i <= 10
.
Me gusta esta definición muy simple: ( fuente )
Un bucle invariante es una condición [entre las variables del programa] que es necesariamente cierta inmediatamente antes e inmediatamente después de cada iteración de un bucle. (Tenga en cuenta que esto no dice nada sobre su verdad o falsedad en parte a través de una iteración).
Por sí solo, un bucle invariante no hace mucho. Sin embargo, dado un invariante apropiado, puede usarse para ayudar a probar la exactitud de un algoritmo. El ejemplo simple en CLRS probablemente tiene que ver con la ordenación. Por ejemplo, deje que su bucle invariante sea algo así, al comienzo del bucle, i
se ordenan las primeras entradas de esta matriz. Si puede probar que esto es, de hecho, un bucle invariante (es decir, que se mantiene antes y después de cada iteración del bucle), puede usar esto para probar la corrección de un algoritmo de clasificación: al final del bucle, el bucle invariante aún está satisfecho , y el contador i
es la longitud de la matriz. Por lo tanto, las primeras i
entradas están ordenadas significa que toda la matriz está ordenada.
Un ejemplo aún más simple: Invariantes de bucles, corrección y derivación de programa .
La forma en que entiendo un bucle invariante es como una herramienta sistemática y formal para razonar sobre los programas. Hacemos una sola afirmación en la que nos enfocamos en demostrar que es verdadera, y la llamamos invariante del ciclo. Esto organiza nuestra lógica. Si bien podemos debatir informalmente sobre la corrección de algún algoritmo, el uso de un bucle invariante nos obliga a pensar con mucho cuidado y garantiza que nuestro razonamiento sea hermético.
Hay una cosa que muchas personas no se dan cuenta de inmediato cuando se trata de bucles e invariantes. Se confunden entre el bucle invariante y el bucle condicional (la condición que controla la terminación del bucle).
Como la gente señala, el bucle invariante debe ser cierto
(aunque puede ser temporalmente falso durante el cuerpo del bucle). Por otro lado, el bucle condicional debe ser falso después de que el bucle termina; de lo contrario, el bucle nunca terminaría.
Por lo tanto, el bucle invariante y el bucle condicional deben ser condiciones diferentes.
Un buen ejemplo de un bucle complejo invariante es para la búsqueda binaria.
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
}
Por lo tanto, el bucle condicional parece bastante sencillo: cuando inicio> fin, el bucle termina. Pero, ¿por qué es correcto el bucle? ¿Cuál es el bucle invariante que demuestra su corrección?
La invariante es la declaración lógica:
if ( A[mid] == a ) then ( start <= mid <= end )
Esta afirmación es una tautología lógica: siempre es cierta en el contexto del bucle / algoritmo específico que estamos tratando de probar . Y proporciona información útil sobre la corrección del bucle después de que finaliza.
Si volvemos porque encontramos el elemento de la matriz a continuación, la declaración es claramente cierto, ya que si A[mid] == a
se a
está en la matriz y mid
debe estar entre inicio y fin. Y si el bucle termina, porque start > end
entonces no puede haber un número tal que start <= mid
y mid <= end
y por lo tanto sabemos que la declaración A[mid] == a
debe ser falsa. Sin embargo, como resultado, la declaración lógica general sigue siendo cierta en sentido nulo. (En lógica, la declaración if (false) then (something) siempre es verdadera).
Ahora, ¿qué pasa con lo que dije sobre el bucle condicional que necesariamente es falso cuando el bucle termina? Parece que cuando el elemento se encuentra en la matriz, entonces el condicional del bucle es verdadero cuando el bucle termina. En realidad no lo es, porque el bucle condicional implícito es realmente while ( A[mid] != a && start <= end )
pero acortamos la prueba real ya que la primera parte está implícita. Este condicional es claramente falso después del ciclo, independientemente de cómo termine el ciclo.
a
esté presente A
. Informalmente sería, "si la clave a
está presente en la matriz, debe ocurrir entre start
e end
inclusive". Luego se deduce que si A[start..end]
está vacío, eso a
no está presente en A.
Las respuestas anteriores han definido un bucle invariante de una muy buena manera.
A continuación se muestra cómo los autores de CLRS usaron el bucle invariante para probar la corrección del tipo de inserción.
Algoritmo de clasificación de inserción (como se da en el libro):
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
Invariante de bucle en este caso: la submatriz [1 a j-1] siempre se ordena.
Ahora verifiquemos esto y demostremos que el algoritmo es correcto.
Inicialización : antes de la primera iteración j = 2. Entonces sub-array [1: 1] es el array a ser probado. Como solo tiene un elemento, está ordenado. Así invariante está satisfecho.
Mantenimiento : Esto puede verificarse fácilmente marcando la invariante después de cada iteración. En este caso está satisfecho.
Terminación : este es el paso donde probaremos la corrección del algoritmo.
Cuando el ciclo termina, entonces el valor de j = n + 1. Nuevamente, el bucle invariante está satisfecho. Esto significa que la sub-matriz [1 a n] debe ordenarse.
Esto es lo que queremos hacer con nuestro algoritmo. Por lo tanto, nuestro algoritmo es correcto.
Además de todas las buenas respuestas, creo que un gran ejemplo de Cómo pensar en algoritmos, de Jeff Edmonds, puede ilustrar muy bien el concepto:
EJEMPLO 1.2.1 "El algoritmo Find-Max de dos dedos"
1) Especificaciones: una instancia de entrada consiste en una lista L (1..n) de elementos. La salida consiste en un índice i tal que L (i) tiene un valor máximo. Si hay varias entradas con este mismo valor, se devuelve cualquiera de ellas.
2) Pasos básicos: usted decide el método de dos dedos. Su dedo derecho recorre la lista.
3) Medida del progreso: la medida del progreso es qué tan lejos de la lista está su dedo derecho.
4) La invariante del bucle: la invariante del bucle indica que su dedo izquierdo apunta a una de las entradas más grandes encontradas hasta ahora con su dedo derecho.
5) Pasos principales: en cada iteración, mueves el dedo derecho hacia abajo una entrada de la lista. Si su dedo derecho ahora apunta a una entrada que es más grande que la entrada del dedo izquierdo, entonces mueva el dedo izquierdo para estar con el derecho.
6) Progresar: progresa porque su dedo derecho mueve una entrada.
7) Mantener invariante de bucle: sabe que la invariante de bucle se ha mantenido de la siguiente manera. Para cada paso, el nuevo elemento del dedo izquierdo es Max (antiguo elemento del dedo izquierdo, nuevo elemento). Por el bucle invariante, este es Max (Max (lista más corta), nuevo elemento). Matemáticamente, este es Max (lista más larga).
8) Establecer la invariante del bucle: Inicialmente se establece la invariante del bucle apuntando ambos dedos al primer elemento.
9) Condición de salida: Ha terminado cuando su dedo derecho ha terminado de recorrer la lista.
10) Final: al final, sabemos que el problema se resuelve de la siguiente manera. Por la condición de salida, su dedo derecho ha encontrado todas las entradas. Por el bucle invariante, su dedo izquierdo apunta al máximo de estos. Devuelve esta entrada.
11) Terminación y tiempo de ejecución: el tiempo requerido es algunas veces constante la longitud de la lista.
12) Casos especiales: verifique qué sucede cuando hay múltiples entradas con el mismo valor o cuando n = 0 o n = 1.
13) Detalles de codificación e implementación: ...
14) Prueba formal: la corrección del algoritmo se deduce de los pasos anteriores.
Cabe señalar que un Loop Invariant puede ayudar en el diseño de algoritmos iterativos cuando se considera una afirmación que expresa relaciones importantes entre las variables que deben ser verdaderas al comienzo de cada iteración y cuando el ciclo termina. Si esto se cumple, el cálculo está en camino a la efectividad. Si es falso, entonces el algoritmo ha fallado.
Invariante en este caso significa una condición que debe ser verdadera en un cierto punto en cada iteración del ciclo.
En la programación por contrato, una invariante es una condición que debe ser verdadera (por contrato) antes y después de que se llame a cualquier método público.
El significado de invariante nunca cambia
Aquí, el invariante del bucle significa "El cambio que sucede a la variable en el bucle (incremento o decremento) no está cambiando la condición del bucle, es decir, la condición es satisfactoria", de modo que el concepto invariante del bucle ha llegado
La propiedad invariante de bucle es una condición que se cumple para cada paso de la ejecución de un bucle (es decir, para bucles, bucles while, etc.)
Esto es esencial para una Prueba Invariante de Bucle, donde uno puede demostrar que un algoritmo se ejecuta correctamente si en cada paso de su ejecución se mantiene esta propiedad invariante de bucle.
Para que un algoritmo sea correcto, la Invariante de bucle debe mantenerse en:
Inicialización (el comienzo)
Mantenimiento (cada paso después)
Terminación (cuando está terminado)
Esto se utiliza para evaluar un montón de cosas, pero el mejor ejemplo son los algoritmos codiciosos para el cruce de gráficos ponderados. Para que un algoritmo codicioso produzca una solución óptima (una ruta a través del gráfico), debe llegar a conectar todos los nodos en la ruta de menor peso posible.
Por lo tanto, la propiedad invariante del bucle es que la ruta tomada tiene el menor peso. Al principio no hemos agregado ningún borde, por lo que esta propiedad es verdadera (no es falsa, en este caso). En cada paso , seguimos el borde de menor peso (el paso codicioso), por lo que nuevamente estamos tomando la ruta de menor peso. Al final , hemos encontrado la ruta más baja ponderada, por lo que nuestra propiedad también es cierta.
Si un algoritmo no hace esto, podemos demostrar que no es óptimo.
Es difícil hacer un seguimiento de lo que sucede con los bucles. Los bucles que no terminan o terminan sin lograr su objetivo de comportamiento es un problema común en la programación de computadoras. Los invariantes de bucle ayudan. Un ciclo invariante es una declaración formal sobre la relación entre las variables en su programa que es verdadera justo antes de que el ciclo se ejecute (estableciendo el invariante) y es verdadera nuevamente en la parte inferior del ciclo, cada vez a través del ciclo (manteniendo el invariante ) Aquí está el patrón general del uso de invariantes de bucle en su código:
... // la Invariante del bucle debe ser verdadera aquí
mientras que (CONDICIÓN DE PRUEBA) {
// parte superior del bucle
...
// parte inferior del bucle
// la Invariante del bucle debe ser verdadera aquí
}
// Terminación + Invariante del bucle = Meta
...
Entre la parte superior e inferior del bucle, presumiblemente se avanza hacia alcanzar la meta del bucle. Esto podría perturbar (hacer falso) lo invariante. El punto de las invariantes de bucle es la promesa de que la invariante se restaurará antes de repetir el cuerpo del bucle cada vez. Hay dos ventajas para esto:
El trabajo no se traslada al siguiente paso de formas complicadas que dependen de los datos. Cada paso a través del bucle es independiente de todos los demás, y el invariante sirve para unir los pases en un todo funcional. El razonamiento de que su ciclo funciona se reduce al razonamiento de que el ciclo invariante se restaura con cada pasada a través del ciclo. Esto divide el complicado comportamiento general del bucle en pequeños pasos simples, cada uno de los cuales puede considerarse por separado. La condición de prueba del bucle no es parte de la invariante. Es lo que hace que el ciclo termine. Considera por separado dos cosas: por qué el ciclo debería terminar alguna vez, y por qué el ciclo logra su objetivo cuando termina. El bucle terminará si cada vez que se acerque al bucle se acerque para satisfacer la condición de terminación. A menudo es fácil asegurar esto: por ejemplo pasando una variable de contador por una hasta que alcance un límite superior fijo. A veces el razonamiento detrás de la terminación es más difícil.
El bucle invariante debe crearse de modo que cuando se alcance la condición de terminación y el invariante sea verdadero, se alcance el objetivo:
invariante + terminación => objetivo
Se necesita práctica para crear invariantes que sean simples y se relacionen y que capturen todo el logro del objetivo, excepto la terminación. Es mejor usar símbolos matemáticos para expresar invariantes de bucle, pero cuando esto lleva a situaciones demasiado complicadas, confiamos en una prosa clara y sentido común.
Lo siento, no tengo permiso para comentar.
@Tomas Petricek como mencionaste
Una invariante más débil que también es cierta es que i> = 0 && i <10 (¡porque esta es la condición de continuación!) "
¿Cómo es un bucle invariante?
Espero no estar equivocado, por lo que entiendo [1] , la invariante de bucle será verdadera al comienzo del bucle (Inicialización), será verdadera antes y después de cada iteración (Mantenimiento) y también será verdadera después La terminación del bucle (terminación) . Pero después de la última iteración, se convierte en 10. Entonces, la condición i> = 0 && i <10 se convierte en falsa y termina el ciclo. Viola la tercera propiedad (Terminación) del bucle invariante.
[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html
Loop invariant es una fórmula matemática como (x=y+1)
. En ese ejemplo, x
y y
representan dos variables en un bucle. Teniendo en cuenta el cambio de comportamiento de esas variables a lo largo de la ejecución del código, es casi imposible probar todo posible x
y y
los valores y ver si se produce ningún error. Digamos que x
es un número entero. El entero puede contener un espacio de 32 bits en la memoria. Si ese número excede, se produce un desbordamiento del búfer. Por lo tanto, debemos asegurarnos de que durante la ejecución del código, nunca exceda ese espacio. para eso, necesitamos entender una fórmula general que muestre la relación entre variables. Después de todo, solo tratamos de entender el comportamiento del programa.
Un bucle invariante es una afirmación que es verdadera antes y después de la ejecución del bucle.
En la búsqueda lineal (según el ejercicio dado en el libro), necesitamos encontrar el valor V en una matriz dada.
Es simple como escanear la matriz desde 0 <= k <longitud y comparar cada elemento. Si se encuentra V, o si el escaneo alcanza la longitud de la matriz, simplemente finalice el ciclo.
Según mi comprensión en el problema anterior
Invariantes de bucle (inicialización): V no se encuentra en la iteración k - 1. Primera iteración, esto sería -1, por lo tanto, podemos decir que V no se encuentra en la posición -1
Mantenimiento: en la próxima iteración, V no encontrado en k-1 es verdadero
Terminación: si V se encuentra en la posición k o k alcanza la longitud de la matriz, finalice el bucle.