1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 2 из 6

Инвариант цикла - три проверки

Цикл делает неизвестное заранее число кругов, и проследить их все нельзя. Приём состоит в том, чтобы вместо всех кругов проверить один - но проверить его так, чтобы отсюда следовали остальные.

Как думаешь, что тут постоянно

summa = 0
for i in range(len(a)):
    summa = summa + a[i]

Переменная summa меняется каждый круг. И всё-таки про неё можно сказать кое-что, что верно ВСЕГДА в начале круга: summa равна сумме первых i элементов. При i = 0 сумма пуста и равна нулю; при i = 3 там a[0] + a[1] + a[2]. Это утверждение и называется инвариантом.

Инвариант цикла - утверждение, связывающее переменные, которое верно перед каждым кругом. Оно и есть замена перебору кругов: если оно верно перед первым кругом и каждый круг его сохраняет, оно верно и после последнего.

Три проверки

Инвариант доказывается ровно тремя шагами. Пропуск любого из них - типовая ошибка, поэтому они перечисляются всегда в одном порядке.

Инициализация. Инвариант верен перед первым кругом. Обычно проверяется подстановкой начальных значений, и обычно на пустом множестве: «сумма первых нуля элементов равна нулю» верно, потому что пустая сумма равна нулю.

Сохранение. Если инвариант верен перед кругом, он верен перед следующим. Это единственный содержательный шаг: здесь читается тело цикла.

Завершение. Когда цикл кончился, инвариант вместе с условием выхода даёт постусловие. Этот шаг объясняет, зачем всё делалось: без него доказано свойство, из которого ничего не следует.

Первые два шага - это индукция: утверждение проверяется для начального случая и для перехода от круга к кругу, а на все круги распространяется само. Следующая глава разбирает её отдельно.

Разбор: сортировка вставками

for j in range(1, len(a)):
    klyuch = a[j]
    i = j - 1
    while i >= 0 and a[i] > klyuch:
        a[i + 1] = a[i]
        i = i - 1
    a[i + 1] = klyuch

Инвариант внешнего цикла: перед кругом с номером j первые j элементов массива упорядочены по неубыванию и состоят из тех же значений, что были в них изначально. Вторая половина - та самая связь со входом, без которой постусловие пропускает мусор.

Инициализация. При j = 1 первый элемент один, один элемент упорядочен, и это тот же самый элемент. Верно.

Сохранение. Тело цикла берёт a[j], сдвигает вправо все элементы левой части, которые больше него, и кладёт ключ в освободившееся место. После этого левая часть длиной j+1 упорядочена: слева от ключа всё не больше него (иначе сдвиг продолжился бы), справа всё больше (их и сдвигали). Значения не потеряны: каждое либо осталось на месте, либо переехало на одну ячейку.

Завершение. Цикл кончается при j = n. Инвариант говорит: первые n элементов упорядочены и являются перестановкой исходных. Это и есть постусловие сортировки. Доказательство закончено.

Обрати внимание, чего в доказательстве нет: перебора примеров, слов «очевидно» и ссылки на то, что программа запускалась.

Разбор: инвариант двоичного поиска

levo = 0
pravo = len(a)
while levo < pravo:
    seredina = (levo + pravo) // 2
    if a[seredina] < x:
        levo = seredina + 1
    else:
        pravo = seredina

Здесь ищется первая позиция, куда можно вставить x, не нарушив порядка. Инвариант: все элементы левее levo строго меньше x, все элементы правее или равные pravo не меньше x.

Инициализация. levo = 0 - слева ничего нет; pravo = n - справа ничего нет. Оба утверждения верны пусто.

Сохранение. Если a[seredina] < x, то по упорядоченности всё до середины включительно строго меньше x, и levo = seredina + 1 инвариант сохраняет. Иначе a[seredina] ≥ x, и по упорядоченности всё от середины и правее не меньше x - pravo = seredina тоже сохраняет.

Завершение. Цикл кончился при levo = pravo. Слева от этой точки всё строго меньше x, справа всё не меньше. Значит это и есть первая позиция, где x может стоять. Постусловие получено.

Инвариант пишется до кода, а не после

Когда инвариант сформулирован, границы цикла и знаки сравнения выводятся из него, а не подбираются. Правило «где ставить +1» перестаёт быть вопросом памяти: +1 ставится там, где без него утверждение станет ложным.

Теперь сам

Сформулируй инвариант для этого цикла и скажи, что даёт шаг завершения.

schet = 0
for x in a:
    if x > 0:
        schet = schet + 1

Ответ. Инвариант: перед очередным кругом schet равен числу положительных элементов среди уже просмотренных. Инициализация: просмотрено ноль элементов, положительных среди них ноль. Сохранение: круг добавляет к просмотренным один элемент и увеличивает счёт ровно тогда, когда этот элемент положителен. Завершение: просмотрены все элементы, значит schet равен числу положительных во всём списке - это и есть постусловие.

Главное

Инвариант - утверждение, верное перед каждым кругом цикла. Доказывается тремя шагами: инициализация, сохранение, завершение. Содержательный шаг один - сохранение; шаг завершения объясняет, ради чего инвариант выбран именно такой.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.