Я читаю "Введение в алгоритм" от CLRS. В главе 2 авторы упоминают "петлевые инварианты". Что такое инвариант цикла?
Что такое циклический инвариант?
Ответ 1
Простыми словами, инвариантом цикла является некоторый предикат (условие), который выполняется для каждой итерации цикла. Например, давайте посмотрим на простой for
цикла, который выглядит следующим образом:
int j = 9;
for(int i=0; i<10; i++)
j--;
В этом примере верно (для каждой итерации), что i + j == 9
. Более слабый инвариант также справедлив в том, что i >= 0 && я <= 10
.
Ответ 2
Мне нравится это очень простое определение: (источник)
Инвариант цикла является условием [среди программных переменных], которое обязательно истинно непосредственно перед и сразу после каждой итерации цикла. (Обратите внимание, что это ничего не говорит об истине или ложности, частично через итерацию).
Сам по себе инвариант цикла не делает многого. Однако, учитывая соответствующий инвариант, его можно использовать, чтобы помочь доказать правильность алгоритма. Простой пример в CLRS, вероятно, связан с сортировкой. Например, пусть ваш инвариант цикла будет чем-то вроде, в начале цикла будут отсортированы первые записи i
этого массива. Если вы можете доказать, что это действительно инвариант цикла (т.е. Он выполняется до и после каждой итерации цикла), вы можете использовать это, чтобы доказать правильность алгоритма сортировки: при завершении цикла инвариант цикла все еще выполняется, а счетчик i
- длина массива. Поэтому первые записи i
сортируются, так как весь массив сортируется.
Еще более простой пример: Инварианты петель, правильность и выведение программ.
То, как я понимаю инвариант цикла, является систематическим формальным инструментом для разбора программ. Мы делаем одно утверждение о том, что мы фокусируемся на доказательстве истины, и мы называем его инвариантом цикла. Это упорядочивает нашу логику. Хотя мы можем так же хорошо спорить о правильности некоторого алгоритма, использование инварианта цикла заставляет нас думать очень осторожно и гарантирует, что наши рассуждения являются воздухонепроницаемыми.
Ответ 3
Есть одна вещь, которую многие люди не сразу понимают при работе с циклами и инвариантами. Они путаются между инвариантом цикла и условием цикла (условие, которое контролирует завершение цикла).
Как указывают люди, инвариант цикла должен быть истинным
- перед запуском цикла
- перед каждой итерацией цикла
- после завершения цикла
(хотя он может временно быть ложным во время цикла цикла). С другой стороны, условие цикла должно быть ложным после завершения цикла, иначе цикл никогда не завершится.
Таким образом, инвариант цикла и условие цикла должны быть разными.
Хорошим примером сложного инварианта цикла является двоичный поиск.
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
}
Таким образом, условие цикла кажется довольно прямым - когда start > end цикл завершается. Но почему петля правильная? Что такое инвариант цикла, который доказывает его правильность?
Инвариант - это логическое выражение:
if ( A[mid] == a ) then ( start <= mid <= end )
Этот оператор является логической тавтологией - он всегда верен в контексте конкретного цикла/алгоритма, который мы пытаемся доказать. И он предоставляет полезную информацию о правильности цикла после его завершения.
Если мы вернемся, потому что мы нашли элемент в массиве, то утверждение явно верно, так как если A[mid] == a
, то a
находится в массиве, а mid
должен находиться между началом и концом. И если цикл завершается, потому что start > end
, то не может быть числа, такого, что start <= mid
и mid <= end
, и поэтому мы знаем, что утверждение A[mid] == a
должно быть ложным. Однако в результате общий логический оператор по-прежнему остается в нулевом смысле. (В логике утверждение if (false) then (something) всегда истинно.)
Теперь как насчет того, что я сказал об условном условии цикла, обязательно является ложным, когда цикл завершается? Похоже, когда элемент найден в массиве, тогда условие цикла является истинным, когда цикл завершается!? На самом деле это не так, потому что подразумеваемый условный цикл - это while ( A[mid] != a && start <= end )
, но мы сокращаем фактический тест, поскольку подразумевается первая часть. Это условие явно ложно после цикла независимо от того, как цикл завершается.
Ответ 4
Предыдущие ответы очень хорошо определили инвариант цикла.
Ниже показано, как авторы CLRS использовали инвариант цикла для доказательства правильности сортировки вставкой.
Алгоритм сортировки вставкой (как указано в книге):
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
Инвариант цикла в этом случае: под-массив [от 1 до j-1] всегда сортируется.
Теперь давайте проверим это и докажем, что алгоритм корректен.
Инициализация: до первой итерации j = 2. Таким образом, под-массив [1:1] - это массив для тестирования. Поскольку у него есть только один элемент, он сортируется. Таким образом, инвариант выполняется.
Обслуживание: это можно легко проверить, проверяя инвариант после каждой итерации. В этом случае это выполняется.
Прекращение: это шаг, на котором мы докажем правильность алгоритма.
Когда цикл заканчивается, значение j = n + 1. Снова цикл-инвариант выполняется. Это означает, что под-массив [от 1 до n] должен быть отсортирован.
Это то, что мы хотим сделать с нашим алгоритмом. Таким образом, наш алгоритм правильный.
Ответ 5
Помимо всех хороших ответов, я думаю, что отличный пример из Как думать об алгоритмах, Джефф Эдмондс может очень хорошо иллюстрировать концепцию:
ПРИМЕР 1.2.1 "Алгоритм двух пальцев Find-Max"
1) Спецификации: входной экземпляр состоит из списка L (1..n) of элементы. Выход состоит из индекса я такого, что L (i) имеет максимум стоимость. Если имеется несколько записей с таким же значением, тогда любое один из них возвращается.
2) Основные шаги: вы выбираете метод с двумя пальцами. Ваш правый палец пробегает список.
3) Мера прогресса: мерой прогресса является то, насколько далеко перечислите свой правый палец.
4) Инвариант Loop: Инвариант цикла указывает, что ваш левый палец указывает на одну из самых больших записей, встречающихся до сих пор вашим правый палец.
5) Основные шаги: каждая итерация, вы перемещаете правый палец вниз по одному запись в списке. Если ваш правый палец теперь указывает на запись это больше, чем левый ввод пальцев, а затем переместите левый палец для вашего правого пальца.
6) Прогресс: вы делаете прогресс, потому что ваш правый палец движется одна запись.
7) Поддерживать Loop Invariant:. Вы знаете, что инвариант цикла поддерживается следующим образом. Для каждого шага новый элемент левого пальца Макс (старый элемент левого пальца, новый элемент). По инварианту цикла, это Макс (Макс (более короткий список), новый элемент). Математически это Макс. (Более длинный список).
8) Создание инверсии цикла:. Сначала вы устанавливаете инвариант цикла, указывая оба пальца на первый элемент.
9) Условие выхода: вы закончили, когда ваш правый палец закончил перемещение списка.
10) Окончание: В конце мы знаем, что проблема решается следующим образом. От условие выхода, ваш правый палец столкнулся со всеми записей. По инварианту цикла ваш левый палец указывает на максимум из этих. Верните эту запись.
11) Окончание и время выполнения: требуется время, которое является константой чем длина списка.
12) Специальные случаи: проверьте, что происходит, когда есть несколько записей с тем же значением или при n = 0 или n = 1.
13) Детали кодирования и реализации:...
14) Формальное доказательство. Правильность алгоритма следует из выше.
Ответ 6
Следует отметить, что инвертор Loop может помочь в разработке итеративных алгоритмов, если рассматривать утверждение, которое выражает важные отношения между переменными, которые должны быть истинными в начале каждой итерации и когда цикл завершается. Если это имеет место, вычисление находится на пути к эффективности. Если false, то алгоритм не удался.
Ответ 7
Инвариантный в этом случае означает условие, которое должно быть истинным в определенной точке каждой итерации цикла.
В контрактном программировании инвариантом является условие, которое должно быть истинным (по контракту) до и после вызова любого публичного метода.
Ответ 8
Значение инварианта никогда не изменяется
Здесь инвариант цикла означает "Изменение, которое происходит с переменной в цикле (приращение или декремент), не меняет условие цикла. Условие удовлетворяет" так, что концепция инварианта цикла пришла
Ответ 9
Трудно отслеживать, что происходит с циклами. Циклы, которые не прекращаются или не заканчиваются без достижения цели, являются общей проблемой в компьютерном программировании. Помощь цикла. Инвариант цикла - это формальное утверждение о взаимосвязи между переменными в вашей программе, которое выполняется как раз перед тем, как цикл будет выполняться (установление инварианта) и снова вернется в нижней части цикла, каждый раз через цикл (поддерживая инвариант). Вот общий пример использования Loop Invariants в вашем коде:
... // Инвариант Loop должен быть истинным здесь
while (ТЕСТОВОЕ СОСТОЯНИЕ) {
// верхняя часть цикла
...
// нижняя часть цикла
// Инвариант Loop должен быть истинным здесь
}
//Termination + Loop Invariant = Цель
...
Между верхней и нижней частью петли, по-видимому, делается шаг вперед к достижению цели петли. Это может нарушить (сделать ложным) инвариант. Точка Инвариантов Loop - это обещание, что инвариант будет восстановлен до повторения тела цикла каждый раз.
Для этого есть два преимущества:
Работа не переносится на следующий проход сложными, зависимыми от данных способами. Каждый проходит через петлю независимо от всех остальных, причем инвариант служит для соединения проходов вместе в рабочее целое. Рассуждение о том, что ваш цикл работает, сводится к обоснованию того, что инвариант цикла восстанавливается с каждым прохождением через цикл. Это нарушает сложное общее поведение цикла на небольшие простые шаги, каждый из которых можно рассматривать отдельно. Условие проверки цикла не является частью инварианта. Это то, что заставляет контур цикла. Вы рассматриваете отдельно две вещи: почему цикл должен заканчиваться, и почему цикл достигает своей цели, когда он завершается. Цикл будет завершаться, если каждый раз через цикл вы приближаетесь к выполнению условия завершения. Часто бывает легко убедиться в этом: например, переключение счетной переменной на единицу, пока не достигнет фиксированного верхнего предела. Иногда рассуждения о прекращении действия сложнее.
Инвариант цикла должен быть создан таким образом, чтобы при достижении условия завершения и инварианте было истинно, тогда цель достигается:
инвариант + окончание = > цель
Требуется практика создания инвариантов, которые являются простыми и связаны между собой, которые захватывают все достижения цели, за исключением прекращения. Лучше всего использовать математические символы для выражения инвариантов цикла, но когда это приводит к чрезмерно сложным ситуациям, мы полагаемся на ясную прозу и здравый смысл.
Ответ 10
Извините, у меня нет комментариев.
@Томас Петричек, как вы упомянули
Более слабый инвариант также справедлив в том, что я >= 0 && я < 10 (потому что это условие продолжения!) "
Как это инвариант цикла?
Надеюсь, что я не ошибаюсь, насколько я понимаю [1] инвариант цикла будет истинным в начале цикла (инициализация), он будет истинным до и после каждой итерации (Техническое обслуживание) и , это также будет истинным после окончания цикла (Termination). Но после последней итерации я становится 10. Итак, условие я >= 0 && я < 10 становится ложным и завершает цикл. Он нарушает третье свойство (Termination) инварианта цикла.
[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html
Ответ 11
Инвариантное свойство Loop является условием, которое выполняется для каждого шага выполнения цикла (т.е. для циклов, а также циклов и т.д.)
Это необходимо для Инвариантного доказательства цикла, где можно показать, что алгоритм выполняется правильно, если на каждом шаге его выполнения это свойство инварианта цикла сохраняется.
Чтобы алгоритм был правильным, Инвариант Loop должен иметь значение:
Инициализация (начало)
Обслуживание (каждый шаг после)
Прекращение (когда оно закончено)
Это используется для оценки множества вещей, но лучшим примером являются жадные алгоритмы для взвешенного обхода графика. Для жадного алгоритма, который дает оптимальное решение (путь по графику), он должен достигнуть соединения всех узлов на самом низком пути веса.
Таким образом, свойство инварианта цикла состоит в том, что принятый путь имеет наименьший вес. В начале мы не добавляем никаких ребер, поэтому это свойство истинно (в этом случае оно не является ложным). На каждом шаге мы выполняем край наименьшего веса (жадный шаг), поэтому снова мы берем самый низкий путь веса. В конце мы нашли самый низкий взвешенный путь, поэтому наше свойство также верно.
Если алгоритм этого не делает, мы можем доказать, что он не является оптимальным.
Ответ 12
Инвариант цикла является математической формулой, такой как (x=y+1)
. В этом примере x
и y
представляют две переменные в цикле. Принимая во внимание изменение поведения этих переменных во время выполнения кода, почти невозможно проверить все возможные значения x
и y
и посмотреть, производят ли они какую-либо ошибку. Допустим, что x
- целое число. Целое число может содержать 32-битное пространство в памяти. Если это число превышает значение, происходит переполнение буфера. Поэтому мы должны быть уверены, что на протяжении всего выполнения кода он никогда не превышает это пространство. для этого нам нужно понять общую формулу, которая показывает взаимосвязь между переменными.
В конце концов, мы просто пытаемся понять поведение программы.
Ответ 13
Простыми словами это условие LOOP истинно в каждой итерации цикла:
for(int i=0; i<10; i++)
{ }
В этом можно сказать, что состояние я равно i<10 and i>=0
Ответ 14
Инвариант цикла - это утверждение, которое верно до и после выполнения цикла.
Ответ 15
В линейном поиске (согласно упражнению, приведенному в книге) нам нужно найти значение V в заданном массиве.
Простое сканирование массива от 0 <= k < длины и сравнения каждого элемента. Если V найден, или если сканирование достигает длины массива, просто завершите цикл.
В соответствии с моим пониманием проблемы выше -
Инварианты цикла (инициализация): V не встречается в k - 1 итерации. Очень первая итерация, это будет -1, поэтому можно сказать, что V не найден в позиции -1
коммунальных: В следующей итерации V, не найденной в k-1, имеет значение true
Terminatation: Если V, найденное в k-позиции или k, достигает длины массива, завершите цикл.