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