Базисные схемы обработки информации
Инвариант и ограничивающая функция цикла
Основная идея метода проектирования цикла при помощи инварианта — выражение взаимосвязи между меняющимися в теле цикла объектами в виде неизменного условия. Польза инварианта состоит в том, что такое описание взаимосвязи легко понимаемо и позволяет, зная, как меняются одни объекты, выводить, как должны изменяться другие. Тем самым инвариант помогает сконструировать команды S0 и S.
Определение 7.1. Инвариант цикла — предикат, который истинен перед выполнением цикла и после каждой его итерации.
Любая тавтология является инвариантом произвольного цикла, однако такие инварианты бесполезны для целей проектирования программ. Вопросы построения инвариантов будут исследованы в следующем параграфе, а сейчас рассмотрим определение еще одного важнейшего понятия.
Для построения корректного условия продолжения цикла e используется ограничивающая функция . При каждом шаге цикла значение должно уменьшаться по крайней мере на единицу и оставаться положительным до завершения цикла. Ограничивающая функция позволяет гарантировать завершение цикла.
Определение 7.2. Ограничивающая функция — неотрицательная функция, являющаяся верхней границей числа оставшихся итераций цикла.
Схема проектирования цикла при помощи инварианта.
На первом этапе выполняются простые присваивания S0, делающие инвариант истинным, а в качестве условия продолжения цикла e берется предикат . На втором этапе конструируется тело цикла S, реализующее преобразование : сначала обеспечивается уменьшение ограничивающей функции , а затем — сохранение инварианта .
Геометрическая интерпретация данной схемы приведена на рис. 7.3, где через обозначено множество, получающееся из с помощью совокупности присваиваний S0.
Рассмотрим применение этой схемы для решения следующей задачи.
Задача 7.3. Напишите программу, перемножающую два целых числа, одно из которых неотрицательно, без использования операции умножения. Точные пред- и постусловия требуемой программы, временная сложность которой не должна превосходить , таковы: , . При написании программы величины и изменять не разрешается, следует использовать инвариант и ограничивающую функция .
Решение Для написания программы надо сконструировать S0 (совокупность присваиваний, осуществляющих начальную инициализацию), условие продолжения e и тело цикла S.
Начальные присваивания должны сделать истинным инвариант (в случае истинности предусловия) и при этом быть максимально простыми и легко находимыми. В данном случае достаточно быстро можно обнаружить, что хорошим кандидатом на роль S0 является следующая совокупность присваиваний "x=a;y=b;z=0;".
Условие продолжения цикла e нам уже по существу дано, так как очередная итерация цикла должна происходить только при . По этой причине логично предположить, что наша программа должна содержать оператор "while(y>0)S;", тело которого S пока неизвестно.
Мы обязаны обеспечить завершение цикла, следовательно величина должна уменьшаться на каждой его итерации. Уменьшение каждый раз на единицу заведомо не позволит получить достаточно эффективную программу. По этой причине необходимо уменьшать величину более быстро. Хорошей мыслью является попытка делить величину пополам тогда, когда это можно (при четных ). Легко оценить, что если бы деление пополам происходило на каждом шаге цикла, то количество его итераций было бы примерно равно . Это вполне согласуется с тем, что требуется в решаемой задаче.
Так как после итерации цикла инвариант должен остаться истинным, то уже зная, как меняется , вычисляем, как следует изменять (ибо по условию задачи мы не можем изменять и ). В результате находим (либо в уме, либо формально вычисляя ), что при четных величину следует удваивать, а при нечетных — необходимо увеличивать значение переменной на .
Текст программы
public class MulI { public static void main(String[] args) throws Exception { int a = Xterm.inputInt("a -> "); int b = Xterm.inputInt("b -> "); int x = a, y = b, z = 0; while (y > 0) { if ((y&1) == 0) { y >>>= 1; x += x; } else { y -= 1; z += x; } } Xterm.println("a * b = " + z); } }