Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Теоретическая информатика. Доказательство правильности. Учебное пособие
.pdf
Глава 3. ДОКАЗАТЕЛЬСТВО ПРАВИЛЬНОСТИ
ПРОГРАММ, НАПИСАННЫХ НА ОБЫЧНЫХ
ЯЗЫКАХ ПРОГРАММИРОВАНИЯ
3.1. ОСНОВНЫЕ ПРИЕМЫ ДОКАЗАТЕЛЬСТВА
ПРАВИЛЬНОСТИ ПРОГРАММ
Метод индуктивных утверждений, о котором шла речь в предыдущей главе, можно непосредственно использовать для доказательства
частичной правильности программ, написанных на каком-нибудь традиционном языке программирования, например на Фортране, Паскале
или Си++. Конечность таких программ можно доказывать так же, как
мы это делали для блок-схем. При использовании метода индуктивных
утверждений необходимо, по крайней мере, с одной из точек программы в каждом из замкнутых путей (циклов) связать соответствующее
утверждение. Конечно, если мы используем язык программирования,
то порядок выполнения (пути) неявно определяется структурой управления, тогда как на блок-схемах этот порядок явно указывается стрелками. Следовательно, применяя метод индуктивных
утверждений,
нужно четко представлять порядок выполнения программы, чтобы
быть уверенным, что не пропущен ни один из замкнутых путей в программе. Именно этот вопрос мы и рассмотрим здесь, так как сам метод
утверждений мы уже описали выше, а его использование в данном
случае аналогично использованию для блок-схем.
Проиллюстрируем метод на
простых программах, написанных на
Фортране.
Пример 3.1
READ (5, 1) J1, J2 (Операторы READ… FORMAT производят ввод
1 FORMAT (2I10) информации по соответствующему формату)
С ЗНАЧЕНИЯ, СЧИТЫВАЕМЫЕ В J1 И J2 –
С ЦЕЛЫЕ, УДОВЛЕТВОРЯЮЩИЕ УСЛОВИЯМ
С 0.LE.J1 И 1.LE.J2. (логические операции: LE – “” ;
IQ=0
IR=J1
2 IF(IR .LT.J2) GO TO 4
LT – “< “
EQ – “=”
GE – “”
С J1 .EQ. IQ * I2 + IR AND 0 .LE. IR GT – “>”
IQ=IQ+1 NE – “”
IR=IR–J2
31

3 GO TO 2 (C – признак комментария)
4 WRITE (6,5) IQ, IR (Операторы WRITE … FORMAT
производят вывод
5 FORMAT (2I10) информации по соответствующему формату)
С JL .EQ. IQ*J2 + IR AND 0.LE. IR AND
С IR .LT. J2
STOP
END
Приведенная программа на Фортране – это просто некоторый вариант программы, блок-схема которой показана на рис. 3. Напомним, что
программа вычисляет целое частное IQ и
остаток IR от деления целого J1 на целое J2. Мы уже включили в качестве комментариев в
программу индуктивные утверждения, необходимые для доказательства частичной правильности. Например, комментарий, помещенный
сразу после оператора с меткой 2, нужно рассматривать как комментарий, связанный с точкой, расположенной перед выполняемой в этом
операторе проверкой. Таким образом
, предполагается, что в момент
прихода в точку, находящуюся непосредственно перед проверкой в
операторе с меткой 2, справедливы утверждения J1 = IQ J2 + IR и
0 IR. Отметим, что в доказательствах мы используем „ = " как символ
равенства, а не присваивания.
После этого доказательство частичной правильности данной про-
граммы почти идентично доказательству частичной правильности
для
блок-схемы на рис. 3. При этом необходимо рассмотреть следующие
пути.
1. Путь от оператора READ до оператора с меткой 2. Предположим, что только что выполнился оператор READ и справедливо
утверждение, записанное сразу после него. Теперь последовательно
выполняются операторы до оператора с меткой 2. Нам нужно показать,
что справедливо утверждение, записанное после этого
оператора. Если
мы дошли до этой точки, то имеем IQ = 0, IR = J1, 0 J1 и 1 J2.
Таким образом, J1=IQ J2 + IR = 0 J2 + J1 = J1 и 0 J1 = IR.
Следовательно, наше утверждение справедливо.
2. Путь от оператора с меткой 2 до оператора с меткой 3 и
опять к оператору с
меткой 2 (основной цикл программы). Предполо-
жим, что мы выполняем оператор 2 (мы перестаем говорить «оператор
с меткой 2» и начинаем употреблять «оператор 2». Здесь уместно от-
32

метить, что если убрать из языка программирования метки, то идентифицировать оператор станет весьма затруднительно) и справедливо
записанное после него утверждение, затем выполняем цикл и возвращаемся к метке 2. Необходимо показать, что указанное утверждение
снова будет справедливо. Пусть IQ и IR до выполнения цикла принимают значения IQ
и IRN. Тогда J1 = IQN J2 + IRN и 0 IRN. При
N
возврате к метке 2 после прохождения цикла значения IQ и IR будут
= IQN + 1 и IR
IQ
N+1
= IRN – J2, а значения J1 и J2 останутся без
N+1
изменений. Таким образом, после возврата к метке 2 имеем
J2 + IR
IQ
N+1
= IQ
J2 + J2 + IRN – J2 = IQN J2 + IRN = J1.
N
= (IQN + 1) J2 + (IRN – J2) =
N+1
Кроме того, известно, что если мы проходили по циклу, то провер-
ка IR
следует, что при возврате к метке 2 имеем 0 IR
.LT. J2 дала отрицательный результат, т. е. J2 IRN. Отсюда
N
– J2 = IR
N
N+1
.
3. Путь от оператора 2 к оператору 4. Предположим, что мы вы-
полнили оператор 2, справедливо соответствующее утверждение, и
переходим к оператору 4. Нужно показать, что справедливо утверждение, записанное ниже этого оператора. Оператор 2 передаст управление на оператор 4 только при положительном результате проверки
IR .LT. J2. При переходе от оператора 2 к оператору 4 ни
одно из зна-
чении переменных не изменяется и, следовательно, при достижении
метки 4 мы имеем J1 = IQ J2 + IR, 0 IR и, кроме того, IR < J2.
Таким образом, мы доказали частичную правильность.
Доказательство конечности программы на Фортране идентично доказательству конечности для блок-схем. Для доказательства конечности программы необходимо только
показать, что при выполнении программы мы в конце концов выйдем из единственного в программе
цикла. Следовательно, надо показать, что проверка IR .LT. J2, управ-
ляющая циклом, когда-нибудь даст положительный результат. Так как
значение IR при каждом прохождении цикла уменьшается на величину J2, a J2 остается без изменений и, кроме
того, 1 J2, то можно
заключить, что IR уменьшается каждый раз, по крайней мере, на 1 и
когда-нибудь станет меньше J2. В этот момент условие IR < J2 станет справедливым и мы выйдем из цикла, т. е. программа закончится.
33

Пример 3.2
T
С1 Х – ВЕЩЕСТВЕННЫЙ МАССИВ, ЕГО РАЗМЕР
С1 N .GE. 2
XSMLST = X(1)
DO 10 I = 2, N
C2 XSMLST РАВНО МИНИМАЛЬНОМУ ИЗ Х(1), ..., Х(I – 1)
С2 КРОМЕ ТОГО, 2 .LE. I И I .LE. N
IF (XSMLST .LE. X(I)) GO TO 10
XSMLST = X(I)
C3 XSMLST РАВНО МИНИМАЛЬНОМУ Х(1), ..., X(I – 1)
C3 КРОМЕ ТОГО, 3 .LE. I И I .LE. N + 1
10 CONTINUE
C4 XSMLST РАВНО МИНИМАЛЬНОМУ ИЗ Х(1),..., X(N)
Предполагается, что приведенный фрагмент программы должен
присвоить XSMLST значение наименьшего элемента из массива Х(1),
... , X(N). В Фортране цикл строится по следующей основной схеме:
DO метка I = M, N
С ПРИМЕЧАНИЕ 1
Тело цикла
I M
Утверждение 1
тело цикла
С ПРИМЕЧАНИЕ 2
метка CONTINUE
I I + 1
I N
Утверждение 2
F
Рис. 10
Эта схема работает аналогично приведенной на рис. 10 блок-схеме.
34

Примечание 1 (комментарий), связанное с таким циклом и стоящее
непосредственно за оператором DO, нужно рассматривать как связанное с некоторой точкой утверждение 1 на блок-схеме, а примечание 2,
стоящее перед концом цикла, нужно рассматривать как утверждение 2
для некоторой точки программы, соответствующей точке на блоксхеме.
Для доказательства частичной правильности программы нужно
рассмотреть следующие пути
.
1. Путь от начала фрагмента до точки 2 (точки, с которой
связано утверждение 2). Если мы попадаем в эту точку, то XSMLST =
= Х(1) и I = 2 и очевидно, что значение XSMLST равно наименьше-
му из Х(1), ..., Х(I – 1) = Х(2 – 1) = Х(1) и 2 I = 2 N (так как N
2). (Обратите внимание, что без
условия N 2 наше утверждение
доказать нельзя и, следовательно, правильная работа программы не
гарантируется, если N = 1.)
2. Путь от точки 2 до точки 3 (точки, с которой связано утвер-
ждение 3). Предположим, что мы достигли точки 2, утверждение 2
справедливо, I имеет значение I
вращаясь в точку 2. В теле цикла XSMLST сравнивается с Х(I
и, если XSMLST X(I
), XSMLST остается без изменений. В против-
N
ном случае значение заменяется на Х(I
что значение XSMLST равно наименьшему из Х(1), ... , Х(I
и мы проходим по телу цикла, воз-
N
). Так как до этого известно,
N
– 1), то
N
)
N
можно легко убедиться, что теперь значение XSMLST будет равно
наименьшему из Х(1), ... , Х(I
точки 3, значение I увеличится на 1, и, таким образом, I
). Однако, прежде чем мы дойдем до
N
= IN + 1;
N+1
следовательно, значение XSMLST снова будет равно наименьшему из
Х(1), ... , Х(I
2 I
N, и, следовательно, в точке 3: 2 3 I
N
) = Х(I
N
– 1). Кроме того, из примечания 2 известно, что
N+1
= IN + 1 N + 1.
N+1
3. Путь из точки 3 к точке 2. Мы пройдем по этому пути, если
только проверка I N даст положительный ответ. При этом условии
справедливость примечания 3 приводит к справедливости примечания
2 в момент достижения этой точки.
4. Путь из точки 3 в точку 4. Мы пройдем по этому пути, если
только проверка 1
N даст отрицательный ответ, т. е. при I > N. Но
так как справедливо утверждение в примечании 3, следовательно,
I = N + 1, и при достижении точки 4 значение XSMLST будет равно
наименьшему из Х(1), ... , Х(I – 1) = X[(N + 1) – 1] = X(N), что и требовалось доказать, т. е. доказана частичная правильность.
35

Конечность этого фрагмента программы очевидна: здесь есть только один цикл, и он является фортрановским, а конечность фортрановского цикла, следует считать свойством, присущим самому языку.
Эти два примера показывают, что метод индуктивных утверждений
можно распространить непосредственно и на доказательства правильности программ, написанных на Фортране. Затруднение заключается
лишь в том
, что управление в программах на Фортране не такое явное,
как в блок-схемах. Это приводит к тому, что в программах легко не
заметить некоторые пути или неправильно их интерпретировать
(например, можно предположить, что в операторе цикла проверка выполняется перед выполнением тела цикла). Кроме того, нужно приписывание индуктивных утверждений
к некоторым точкам в программе
на Фортране проводить очень внимательно и все время помнить, с какой неявной точкой связано соответствующее утверждение. Например,
один-единственный оператор Фортрана, такой, как оператор DO, фактически состоит из нескольких действий: установки счетчика, увеличения счетчика и проверки. При доказательстве правильности мы
должны четко осознавать,
с какой из этих точек связано индуктивное
утверждение. Использование переходов GO TO в программах на Фортране тоже порождает проблемы. Если в программе много переходов и
при их использовании не придерживались некоторой дисциплины, то
программу часто трудно понимать и трудно доказывать ее правильность. Это объясняется тем, что сложно проследить за всеми возмож
ными путями выполнения программы и указать нужные для этих путей
индуктивные утверждения. Однако существует и иная точка зрения.
Если у нас есть только переходы, то программа на Фортране становится полностью аналогичной блок-схеме и доказательство правильности
проходит так же легко, как и для блок-схем, ибо мы «видим» все
точки
с утверждениями. Но переходы действительно усугубляют трудности
«неявных» точек, например в операторе цикла.
-
3.2. ЗАДАНИЯ ДЛЯ САМОСТОЯТЕЛЬНОЙ РАБОТЫ
На одном из языков программирования напишите программу по
предлагаемым ниже вариантам, расставьте индуктивные утверждения
и докажите ее правильность. Блок-схемы программ приведены в [4,
рис. 2.3–2.5].
1. Вычисление произведения M на N при условии, что операцию.
умножения использовать нельзя, M и N имеют целые значения и M 0.
36

2. Вычисление и печать значения M N при условии, что M и N
имеют целые значения и M 1, N 1.
3. Вычисление и печать значения M! = 1 2 3 (M – 1) M при
условии, что в качестве M вводится положительное целое.
4. Поиск минимального элемента одномерного массива.
3.3. КОНТРОЛЬНЫЕ ВОПРОСЫ К ГЛАВЕ 3
Тема «Доказательство правильности программ»
1. Можно ли применять для доказательства правильности программ
математическую индукцию?
2. Можно ли применять метод индуктивных утверждений для дока-
зательства правильности программ?
3. Как в программе записываются индуктивные утверждения?
4. Назовите три основных отличия доказательства правильности
программ от доказательства правильности блок-схем.
Глава 4. ДОКАЗАТЕЛЬСТВО ПРАВИЛЬНОСТИ
РЕКУРСИВНЫХ ПРОГРАММ
Во многих языках программирования разрешается писать рекурсивные программы, т. е. программы, в которых при вычислениях допускается обращение (рекурсивное) к самой себе аналогично тому, как
обычная нерекурсивная программа обращается к некоторой подпрограмме. Рекурсивные программы особенно удобны в тех случаях, когда речь идет о данных, структура которых определяется
(например, списки или деревья). В языке Лисп, созданном специально
для «нечисленных» вычислений на основе списков, понятие рекурсии
является фундаментальным. В этой главе описаны методы, которые
полезны при доказательстве правильности рекурсивных программ. Основной метод в таких доказательствах называется методом структурной индукции. Доказательство методом структурной индукции представляет собой
но индукция осуществляется по структуре данных, с которыми работает программа.
доказательство с помощью математической индукции,
рекурсивно
37

Если студент незнаком с понятием рекурсии, он все равно сможет
понимать излагаемый здесь материал. Мы не будем пользоваться каким-нибудь стандартным рекурсивным языком программирования,
например, Алголом или Лиспом, а «изобретем» упрощенный язык, которого нам будет достаточно, чтобы проиллюстрировать понятие рекурсии.
4.1. УПРОЩЕННЫЙ ЯЗЫК ПРОГРАММИРОВАНИЯ
ДЛЯ ИЛЛЮСТРАЦИИ ПОНЯТИЯ РЕКУРСИИ
Упрощенный язык программирования нужен нам, чтобы ознакомить студентов с понятием рекурсии. Если студент знаком с рекурсией
в каком-либо из существующих языков программирования, ему будет
очень просто сопоставить наш упрощенный язык с соответствующими
возможностями в этих реальных языках. Однако, если вы даже и незнакомы с понятием рекурсии из существующих
языков, это не поме-
шает вам понять содержание настоящего раздела.
Программа в нашем упрощенном языке программирования состоит
из определения функции, заданного в виде
F(Х1, ... , ХN) IF проверка 1 THEN выражение 1
ELSE IF проверка 2 THEN выражение 2
…
ELSE IF проверка m THEN выражение m
ELSE OTHERWISE выражение m + 1
Такая программа вычисляет значение F(Х1, ... , ХN). Сначала выполняется проверка 1. Если
проверка 1 дает ответ «истина», то значение функции есть значение выражения 1. Если проверка 1 дает ответ
«ложь», то выполняется проверка 2. Если эта проверка дает ответ «истина», то значение функции есть значение выражения 2. Процесс идет
таким образом до тех пор, пока не будет обнаружена первая из прове-
i-я), дающая значение «истина». Значение функции в этом случае
рок (
есть значение выражения i. Если ни одна из проверок не окажется
положительной, то значение функции есть значение выражения m + 1.
Рекурсия вводится в этот язык программирования следующим образом: функция F, определяемая в нашей программе, может входить как
38

часть в любое из выражений или проверок в программе. Такое появление F называется рекурсивным обращением к этой функции. Для каждой такой рекурсивной программы нужно еще указать, для каких значений аргументов X1, ... , ХN применима программа, т. е. нужно задать
область определения функции. Выполнение программы заключается в
применении ее к некоторым конкретным
данным из ее области опре-
деления.
Пример 4.1. Рассмотрим рекурсивную программу, определенную
для любого положительного целого числа X:
F(Х) IF Х = 1 THEN 1
ELSE OTHERWISE X•F(Х–1)
Чтобы понять, как работает такая программа, выполним ее для не-
которого конкретного значения аргумента X. Например,
F(4) = 4 • F(3) [Так как условие 4 = 1 ложно, то F(4) = 4•F(3).
Теперь нужно вычислить F(3), т. е. рекурсивно
обратиться к F.]
= 4 • 3 • F(2) [Так как при вычислении F(3) условие 3 = 1 ложно,
то F(3) = 3•F(2).]
= 4 • 3 • 2 • F(1) [Так как условие 2 = 1 ложно, то F(2) = 2•F(1).]
= 4 • 3 • 2 • 1 [Так как условие 1 = 1 истинно, то F(1) = 1.]
= 24 = 4 !
Далее мы докажем, что F(Х) = Х! для любого положительного це-
лого числа X.
Пример 4.2. Рассмотрим следующую рекурсивную функцию
(функцию Аккермана), применимую для любой пары неотрицательных
целых чисел X1, Х2:
А(Х1, Х2) IF X1 = 0 THEN Х2 + 1
ELSE IF Х2=0 THEN А(Х1–1, 1)
ELSE OTHERWISE А(Х1–1, А(Х1, Х2–1))
Проследим, как выполняется такая программа для некоторой пары
X1 и Х2.
39

А(1, 2) = А(0, А(1, 1)) [Так как условия X1 = 0 и Х2 = 0
ложны, необходимо вычислять А(1, 1) –
рекурсивное обращение.]
= А(0, А(0, А(1, 0))) [Так как условия X1 = 0 и Х2 = 0 ложны,
нужно вычислять А(1, 0).]
= А(0, А(0, А(0, 1))) [Так как условие X1 = 0 ложно,
а Х2 = 0 истинно.]
= А(0, А(0, 2)) [Так как X1 = 0 истинно,
следовательно, А(0, 1) = 1 + 1 = 2.]
= А(0, 3) [Так как X1 = 0
истинно, следовательно,
А(0, 2) = 2 + 1 = 3.]
= 4 [Так как X1 = 0 истинно, следовательно,
А(0, 3) = 3 + 1 = 4.]
Из этого примера следует, что при выполнении вычислений для рекурсивной программы мы можем сталкиваться с неоднозначностями,
которые требуют уточнений. Например, в приведенных вычислениях
при обращении А(0, А(1, 1)) мы предполагаем, что А(1, 1) должно
быть вычислено прежде, чем мы продолжим вычисления, относящиеся
к внешнему обращению к А. Если отдать предпочтение вычислениям,
связанным с внешним обращением, то вычисления должны были бы
идти в следующем порядке:
А(1, 2) = А(0, А(1, 1)) = А(1, 1) + 1 = А(0, А(1, 0)) + 1 =
= (А(1, 0) + 1) + 1 = (А(0, 1) + 1) + 1 = (2 + 1) + 1 = 4.
Значение А(1, 2) остается тем же, хотя последовательность вычислений отлична от предыдущей. Можно доказать, что если к одним и
же аргументам некоторой рекурсивной функции применять две
тем
различные последовательности ее вычисления, то результат будет одним и тем же при условии конечности этих последовательностей. Однако вполне возможно, что одна последовательность не будет заканчиваться, хотя другая последовательность закончится. Рассмотрим следующий пример.
40
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
